Ваш интерес к новой книге "Секреты 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'# выдача MyCustomErrorraise 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
,
автоматически вызываемые в процессе обработки исключений. Только от
вас зависит, что именно будет делать ваш собственный класс
исключений. Среди показанных методов такие, что отвечают за
инспектирование объекта и вывод на экран информативного сообщения
об ошибке. Как бы то ни было, классы исключений значительно
упрощают обработку всех возникающих ошибок!