https://habr.com/ru/company/piter/blog/537642/- Блог компании Издательский дом «Питер»
- Python
- Программирование
- Проектирование и рефакторинг
- Профессиональная литература
Привет, Хабр!
Ваш интерес к новой книге "
Секреты Python Pro" убедил нас, что рассказ о необычностях Python заслуживает продолжения. Сегодня предлагаем почитать небольшой туториал о создании кастомных (в тексте — собственных) классах исключений. У автора получилось интересно, сложно не согласиться с ним в том, что важнейшим достоинством исключения является полнота и ясность выдаваемого сообщения об ошибке. Часть кода из оригинала — в виде картинок.
Добро пожаловать под кат.
Создание собственных классов ошибок
В Python предусмотрена возможность создавать собственные классы исключений. Создавая такие классы, можно разнообразить дизайн классов в приложении. Собственный класс ошибок мог бы логировать ошибки, инспектировать объект. Это мы определяем, что делает класс исключений, хотя, обычно собственный класс едва ли сможет больше, чем просто отобразить сообщение.
Естественно, важен и сам тип ошибки, и мы часто создаем собственные типы ошибок, чтобы обозначить конкретную ситуацию, которая обычно не покрывается на уровне языка Python. Таким образом, пользователи класса, встретив такую ошибку, будут в точности знать, что происходит.
Эта статья состоит из двух частей. Сначала мы определим класс исключений сам по себе. Затем продемонстрируем, как можно интегрировать собственные классы исключений в наши программы на Python и покажем, как таким образом повысить удобство работы с теми классами, что мы проектируем.
Собственный класс исключений MyCustomError
При выдаче исключения требуются методы __init__()
и __str__()
.
При выдаче исключения мы уже создаем экземпляр исключения и в то же время выводим его на экран. Давайте детально разберем наш собственный класс исключений, показанный ниже.
В вышеприведенном классе MyCustomError есть два волшебных метода,
__init__
и
__str__
, автоматически вызываемых в процессе обработки исключений. Метод
Init
вызывается при создании экземпляра, а метод
str
– при выводе экземпляра на экран. Следовательно, при выдаче исключения два этих метода обычно вызываются сразу друг за другом. Оператор вызова исключения в Python переводит программу в состояние ошибки.
В списке аргументов метода
__init__
есть
*args
. Компонент
*args
– это особый режим сопоставления с шаблоном, используемый в функциях и методах. Он позволяет передавать множественные аргументы, а переданные аргументы хранит в виде кортежа, но при этом позволяет вообще не передавать аргументов.
В нашем случае можно сказать, что, если конструктору
MyCustomError
были переданы какие-либо аргументы, то мы берем первый переданный аргумент и присваиваем его атрибуту
message
в объекте. Если ни одного аргумента передано не было, то атрибуту
message
будет присвоено значение
None
.
В первом примере исключение
MyCustomError
вызывается без каких-либо аргументов, поэтому атрибуту
message
этого объекта присваивается значение
None
. Будет вызван метод
str
, который выведет на экран сообщение ‘MyCustomError message has been raised’.
Исключение
MyCustomError
выдается без каких-либо аргументов (скобки пусты). Иными словами, такая конструкция объекта выглядит нестандартно. Но это просто синтаксическая поддержка, оказываемая в Python при выдаче исключения.
Во втором примере
MyCustomError
передается со строковым аргументом ‘We have a problem’. Он устанавливается в качестве атрибута
message
у объекта и выводится на экран в виде сообщения об ошибке, когда выдается исключение.
Код для класса исключения MyCustomError находится
здесь.
class MyCustomError(Exception):
def __init__(self, *args):
if args:
self.message = args[0]
else:
self.message = None
def __str__(self):
print('calling str')
if self.message:
return 'MyCustomError, {0} '.format(self.message)
else:
return 'MyCustomError has been raised'
# выдача MyCustomError
raise MyCustomError('We have a problem')
Класс CustomIntFloatDic
Создаем собственный словарь, в качестве значений которого могут использоваться только целые числа и числа с плавающей точкой.
Пойдем дальше и продемонстрируем, как с легкостью и пользой внедрять классы ошибок в наши собственные программы. Для начала предложу слегка надуманный пример. В этом вымышленном примере я создам собственный словарь, который может принимать в качестве значений только целые числа или числа с плавающей точкой.
Если пользователь попытается задать в качестве значения в этом словаре любой другой тип данных, то будет выдано исключение. Это исключение сообщит пользователю полезную информацию о том, как следует использовать данный словарь. В нашем случае это сообщение прямо информирует пользователя, что в качестве значений в данном словаре могут задаваться только целые числа или числа с плавающей точкой.
Создавая собственный словарь, нужно учитывать, что в нем есть два места, где в словарь могут добавляться значения. Во-первых, это может происходить в методе init при создании объекта (на данном этапе объекту уже могут быть присвоены ключи и значения), а во-вторых — при установке ключей и значений прямо в словаре. В обоих этих местах требуется написать код, гарантирующий, что значение может относиться только к типу
int
или
float
.
Для начала определю класс CustomIntFloatDict, наследующий от встроенного класса
dict
.
dict
передается в списке аргументов, которые заключены в скобки и следуют за именем класса
CustomIntFloatDict
.
Если создан экземпляр класса
CustomIntFloatDict
, причем, параметрам ключа и значения не передано никаких аргументов, то они будут установлены в
None
. Выражение
if
интерпретируется так: если или ключ равен
None
, или значение равно
None
, то с объектом будет вызван метод
get_dict()
, который вернет атрибут
empty_dict
; такой атрибут у объекта указывает на пустой список. Помните, что атрибуты класса доступны у всех экземпляров класса.
Назначение этого класса — позволить пользователю передать список или кортеж с ключами и значениями внутри. Если пользователь вводит список или кортеж в поисках ключей и значений, то два эти перебираемых множества будут сцеплены при помощи функции
zip
языка Python. Подцепленная переменная, указывающая на объект
zip
, поддается перебору, а кортежи поддаются распаковке. Перебирая кортежи, я проверяю, является ли val экземпляром класса
int
или
float
. Если
val
не относится ни к одному из этих классов, я выдаю собственное исключение
IntFloatValueError
и передаю ему val в качестве аргумента.
Класс исключений IntFloatValueError
При выдаче исключения
IntFloatValueError
мы создаем экземпляр класса
IntFloatValueError
и одновременно выводим его на экран. Это означает, что будут вызваны волшебные методы
init
и
str
.
Значение, спровоцировавшее выдаваемое исключение, устанавливается в качестве атрибута
value
, сопровождающего класс
IntFloatValueError
. При вызове волшебного метода str пользователь получает сообщение об ошибке, информирующее, что значение
init
в
CustomIntFloatDict
является невалидным. Пользователь знает, что делать для исправления этой ошибки.
Классы исключений
IntFloatValueError
и
KeyValueConstructError
Если ни одно исключение не выдано, то есть, все
val
из сцепленного объекта относятся к типам
int
или
float
, то они будут установлены при помощи
__setitem__()
, и за нас все сделает метод из родительского класса
dict
, как показано ниже.
Класс KeyValueConstructError
Что произойдет, если пользователь введет тип, не являющийся списком или кортежем с ключами и значениями?
Опять же, этот пример немного искусственный, но с его помощью удобно показать, как можно использовать собственные классы исключений.
Если пользователь не укажет ключи и значения как список или кортеж, то будет выдано исключение
KeyValueConstructError
. Цель этого исключения – проинформировать пользователя, что для записи ключей и значений в объект
CustomIntFloatDict
, список или кортеж должен быть указан в конструкторе
init
класса
CustomIntFloatDict
.
В вышеприведенном примере, в качестве второго аргумента конструктору
init
было передано множество, и из-за этого было выдано исключение
KeyValueConstructError
. Польза выведенного сообщения об ошибке в том, что отображаемое сообщение об ошибке информирует пользователя: вносимые ключи и значения должны сообщаться в качестве либо списка, либо кортежа.
Опять же, когда выдано исключение, создается экземпляр KeyValueConstructError, и при этом ключ и значения передаются в качестве аргументов конструктору KeyValueConstructError. Они устанавливаются в качестве значений атрибутов key и value у KeyValueConstructError и используются в методе __str__ для генерации информативного сообщения об ошибке при выводе сообщения на экран.
Далее я даже включаю типы данных, присущие объектам, добавленным к конструктору
init
– делаю это для большей ясности.
Установка ключа и значения в CustomIntFloatDict
CustomIntFloatDict
наследует от
dict
. Это означает, что он будет функционировать в точности как словарь, везде за исключением тех мест, которые мы выберем для точечного изменения его поведения.
__setitem__
— это волшебный метод, вызываемый при установке ключа и значения в словаре. В нашей реализации
setitem
мы проверяем, чтобы значение относилось к типу
int
или
float
, и только после успешной проверки оно может быть установлено в словаре. Если проверка не пройдена, то можно еще раз воспользоваться классом исключения
IntFloatValueError
. Здесь можно убедиться, что, попытавшись задать строку
‘bad_value’
в качестве значения в словаре
test_4
, мы получим исключение.
Весь код к этому руководству показан ниже и
выложен на Github.
# Создаем словарь, значениями которого могут служить только числа типов int и float
class IntFloatValueError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return '{} is invalid input, CustomIntFloatDict can only accept ' \
'integers and floats as its values'.format(self.value)
class KeyValueContructError(Exception):
def __init__(self, key, value):
self.key = key
self.value = value
def __str__(self):
return 'keys and values need to be passed as either list or tuple' + '\n' + \
' {} is of type: '.format(self.key) + str(type(self.key)) + '\n' + \
' {} is of type: '.format(self.value) + str(type(self.value))
class CustomIntFloatDict(dict):
empty_dict = {}
def __init__(self, key=None, value=None):
if key is None or value is None:
self.get_dict()
elif not isinstance(key, (tuple, list,)) or not isinstance(value, (tuple, list)):
raise KeyValueContructError(key, value)
else:
zipped = zip(key, value)
for k, val in zipped:
if not isinstance(val, (int, float)):
raise IntFloatValueError(val)
dict.__setitem__(self, k, val)
def get_dict(self):
return self.empty_dict
def __setitem__(self, key, value):
if not isinstance(value, (int, float)):
raise IntFloatValueError(value)
return dict.__setitem__(self, key, value)
# тестирование
# test_1 = CustomIntFloatDict()
# print(test_1)
# test_2 = CustomIntFloatDict({'a', 'b'}, [1, 2])
# print(test_2)
# test_3 = CustomIntFloatDict(('x', 'y', 'z'), (10, 'twenty', 30))
# print(test_3)
# test_4 = CustomIntFloatDict(('x', 'y', 'z'), (10, 20, 30))
# print(test_4)
# test_4['r'] = 1.3
# print(test_4)
# test_4['key'] = 'bad_value'
Заключение
Если создавать собственные исключения, то работать с классом становится гораздо удобнее. В классе исключения должны быть волшебные методы
init
и
str
, автоматически вызываемые в процессе обработки исключений. Только от вас зависит, что именно будет делать ваш собственный класс исключений. Среди показанных методов – такие, что отвечают за инспектирование объекта и вывод на экран информативного сообщения об ошибке.
Как бы то ни было, классы исключений значительно упрощают обработку всех возникающих ошибок!