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