Привет, Хабр!
Ваш интерес к новой книге «Секреты 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
, автоматически вызываемые в процессе обработки исключений. Только от вас зависит, что именно будет делать ваш собственный класс исключений. Среди показанных методов – такие, что отвечают за инспектирование объекта и вывод на экран информативного сообщения об ошибке.
Как бы то ни было, классы исключений значительно упрощают обработку всех возникающих ошибок!
Данный урок посвящен исключениям и работе с ними. Основное внимание уделено понятию исключения в языках программирования, обработке исключений в Python, их генерации и созданию пользовательских исключений.
Исключения в языках программирования
Исключениями (exceptions) в языках программирования называют проблемы, возникающие в ходе выполнения программы, которые допускают возможность дальнейшей ее работы в рамках основного алгоритма. Типичным примером исключения является деление на ноль, невозможность считать данные из файла (устройства), отсутствие доступной памяти, доступ к закрытой области памяти и т.п. Для обработки таких ситуаций в языках программирования, как правило, предусматривается специальный механизм, который называется обработка исключений (exception handling).
Исключения разделяют на синхронные и асинхронные. Синхронные исключения могут возникнуть только в определенных местах программы. Например, если у вас есть код, который открывает файл и считывает из него данные, то исключение типа “ошибка чтения данных” может произойти только в указанном куске кода. Асинхронные исключения могут возникнуть в любой момент работы программы, они, как правило, связаны с какими-либо аппаратными проблемами, либо приходом данных. В качестве примера можно привести сигнал отключения питания.
В языках программирования чаще всего предусматривается специальный механизм обработки исключений. Обработка может быть с возвратом, когда после обработки исключения выполнение программы продолжается с того места, где оно возникло. И обработка без возврата, в этом случае, при возникновении исключения, осуществляется переход в специальный, заранее подготовленный, блок кода.
Различают структурную и неструктурную обработку исключений. Неструктурная обработка предполагает регистрацию функции обработчика для каждого исключения, соответственно данная функция будет вызвана при возникновении конкретного исключения. Для структурной обработки язык программирования должен поддерживать специальные синтаксические конструкции, которые позволяют выделить код, который необходимо контролировать и код, который нужно выполнить при возникновении исключительной ситуации.
В Python выделяют два различных вида ошибок: синтаксические ошибки и исключения.
Синтаксические ошибки в Python
Синтаксические ошибки возникают в случае если программа написана с нарушениями требований Python к синтаксису. Определяются они в процессе парсинга программы. Ниже представлен пример с ошибочным написанием функции print.
>>> for i in range(10): prin("hello!") Traceback (most recent call last): File "<pyshell#2>", line 2, in <module> prin("hello!") NameError: name 'prin' is not defined
Исключения в Python
Второй вид ошибок – это исключения. Они возникают в случае если синтаксически программа корректна, но в процессе выполнения возникает ошибка (деление на ноль и т.п.). Более подробно про понятие исключения написано выше, в разделе “исключения в языках программирования”.
Пример исключения ZeroDivisionError, которое возникает при делении на 0.
>>> a = 10 >>> b = 0 >>> c = a / b Traceback (most recent call last): File "<pyshell#5>", line 1, in <module> c = a / b ZeroDivisionError: division by zero
В Python исключения являются определенным типом данных, через который пользователь (программист) получает информацию об ошибке. Если в коде программы исключение не обрабатывается, то приложение останавливается и в консоли печатается подробное описание произошедшей ошибки с указанием места в программе, где она произошла и тип этой ошибки.
Иерархия исключений в Python
Существует довольно большое количество встроенных типов исключений в языке Python, все они составляют определенную иерархию, которая выглядит так, как показано ниже.
BaseException
+– SystemExit
+– KeyboardInterrupt
+– GeneratorExit
+– Exception
+– StopIteration
+– StopAsyncIteration
+– ArithmeticError
| +– FloatingPointError
| +– OverflowError
| +– ZeroDivisionError
+– AssertionError
+– AttributeError
+– BufferError
+– EOFError
+– ImportError
+– ModuleNotFoundError
+– LookupError
| +– IndexError
| +– KeyError
+– MemoryError
+– NameError
| +– UnboundLocalError
+– OSError
| +– BlockingIOError
| +– ChildProcessError
| +– ConnectionError
| | +– BrokenPipeError
| | +– ConnectionAbortedError
| | +– ConnectionRefusedError
| | +– ConnectionResetError
| +– FileExistsError
| +– FileNotFoundError
| +– InterruptedError
| +– IsADirectoryError
| +– NotADirectoryError
| +– PermissionError
| +– ProcessLookupError
| +– TimeoutError
+– ReferenceError
+– RuntimeError
| +– NotImplementedError
| +– RecursionError
+– SyntaxError
| +– IndentationError
| +– TabError
+– SystemError
+– TypeError
+– ValueError
| +– UnicodeError
| +– UnicodeDecodeError
| +– UnicodeEncodeError
| +– UnicodeTranslateError
+– Warning
+– DeprecationWarning
+– PendingDeprecationWarning
+– RuntimeWarning
+– SyntaxWarning
+– UserWarning
+– FutureWarning
+– ImportWarning
+– UnicodeWarning
+– BytesWarning
+– ResourceWarning
Как видно из приведенной выше схемы, все исключения являются подклассом исключения BaseException. Более подробно об иерархии исключений и их описании можете прочитать здесь.
Обработка исключений в Python
Обработка исключений нужна для того, чтобы приложение не завершалось аварийно каждый раз, когда возникает исключение. Для этого блок кода, в котором возможно появление исключительной ситуации необходимо поместить во внутрь синтаксической конструкции try…except.
print("start") try: val = int(input("input number: ")) tmp = 10 / val print(tmp) except Exception as e: print("Error! " + str(e)) print("stop")
В приведенной выше программе возможных два вида исключений – это ValueError, возникающее в случае, если на запрос программы “введите число”, вы введете строку, и ZeroDivisionError – если вы введете в качестве числа 0.
Вывод программы при вводе нулевого числа будет таким.
start input number: 0 Error! stop
Если бы инструкций try…except не было, то при выбросе любого из исключений программа аварийно завершится.
print("start") val = int(input(“input number: “)) tmp = 10 / val print(tmp) print("stop")
Если ввести 0 на запрос приведенной выше программы, произойдет ее остановка с распечаткой сообщения об исключении.
start
input number: 0
Traceback (most recent call last):
File “F:/work/programming/python/devpractice/tmp.py”, line 3, in <module>
tmp = 10 / val
ZeroDivisionError: division by zero
Обратите внимание, надпись stop уже не печатается в конце вывода программы.
Согласно документу по языку Python, описывающему ошибки и исключения, оператор try работает следующим образом:
- Вначале выполняется код, находящийся между операторами try и except.
- Если в ходе его выполнения исключения не произошло, то код в блоке except пропускается, а код в блоке try выполняется весь до конца.
- Если исключение происходит, то выполнение в рамках блока try прерывается и выполняется код в блоке except. При этом для оператора except можно указать, какие исключения можно обрабатывать в нем. При возникновении исключения, ищется именно тот блок except, который может обработать данное исключение.
- Если среди except блоков нет подходящего для обработки исключения, то оно передается наружу из блока try. В случае, если обработчик исключения так и не будет найден, то исключение будет необработанным (unhandled exception) и программа аварийно остановится.
Для указания набора исключений, который должен обрабатывать данный блок except их необходимо перечислить в скобках (круглых) через запятую после оператора except.
Если бы мы в нашей программе хотели обрабатывать только ValueError и ZeroDivisionError, то программа выглядела бы так.
print("start") try: val = int(input("input number: ")) tmp = 10 / val print(tmp) except(ValueError, ZeroDivisionError): print("Error!") print("stop")
Или так, если хотим обрабатывать ValueError, ZeroDivisionError по отдельность, и, при этом, сохранить работоспособность при возникновении исключений отличных от вышеперечисленных.
print("start") try: val = int(input("input number: ")) tmp = 10 / val print(tmp) except ValueError: print("ValueError!") except ZeroDivisionError: print("ZeroDivisionError!") except: print("Error!") print("stop")
Существует возможность передать подробную информацию о произошедшем исключении в код внутри блока except.
rint("start") try: val = int(input("input number: ")) tmp = 10 / val print(tmp) except ValueError as ve: print("ValueError! {0}".format(ve)) except ZeroDivisionError as zde: print("ZeroDivisionError! {0}".format(zde)) except Exception as ex: print("Error! {0}".format(ex)) print("stop")
Использование finally в обработке исключений
Для выполнения определенного программного кода при выходе из блока try/except, используйте оператор finally.
try: val = int(input("input number: ")) tmp = 10 / val print(tmp) except: print("Exception") finally: print("Finally code")
Не зависимо от того, возникнет или нет во время выполнения кода в блоке try исключение, код в блоке finally все равно будет выполнен.
Если необходимо выполнить какой-то программный код, в случае если в процессе выполнения блока try не возникло исключений, то можно использовать оператор else.
try: f = open("tmp.txt", "r") for line in f: print(line) f.close() except Exception as e: print(e) else: print("File was readed")
Генерация исключений в Python
Для принудительной генерации исключения используется инструкция raise.
Самый простой пример работы с raise может выглядеть так.
try: raise Exception("Some exception") except Exception as e: print("Exception exception " + str(e))
Таким образом, можно “вручную” вызывать исключения при необходимости.
Пользовательские исключения (User-defined Exceptions) в Python
В Python можно создавать собственные исключения. Такая практика позволяет увеличить гибкость процесса обработки ошибок в рамках той предметной области, для которой написана ваша программа.
Для реализации собственного типа исключения необходимо создать класс, являющийся наследником от одного из классов исключений.
class NegValException(Exception): pass try: val = int(input("input positive number: ")) if val < 0: raise NegValException("Neg val: " + str(val)) print(val + 10) except NegValException as e: print(e)
P.S.
Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas. На нашем сайте вы можете найти вводные уроки по этой теме. Все уроки по библиотеке Pandas собраны в книге “Pandas. Работа с данными”.
<<< Python. Урок 10. Функции в Python Python. Урок 12. Ввод-вывод данных. Работа с файлами>>>
How do I manually throw/raise an exception in Python?
Use the most specific Exception constructor that semantically fits your issue.
Be specific in your message, e.g.:
raise ValueError('A very specific bad thing happened.')
Don’t raise generic exceptions
Avoid raising a generic Exception
. To catch it, you’ll have to catch all other more specific exceptions that subclass it.
Problem 1: Hiding bugs
raise Exception('I know Python!') # Don't! If you catch, likely to hide bugs.
For example:
def demo_bad_catch():
try:
raise ValueError('Represents a hidden bug, do not catch this')
raise Exception('This is the exception you expect to handle')
except Exception as error:
print('Caught this error: ' + repr(error))
>>> demo_bad_catch()
Caught this error: ValueError('Represents a hidden bug, do not catch this',)
Problem 2: Won’t catch
And more specific catches won’t catch the general exception:
def demo_no_catch():
try:
raise Exception('general exceptions not caught by specific handling')
except ValueError as e:
print('we will not catch exception: Exception')
>>> demo_no_catch()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in demo_no_catch
Exception: general exceptions not caught by specific handling
Best Practices: raise
statement
Instead, use the most specific Exception constructor that semantically fits your issue.
raise ValueError('A very specific bad thing happened')
which also handily allows an arbitrary number of arguments to be passed to the constructor:
raise ValueError('A very specific bad thing happened', 'foo', 'bar', 'baz')
These arguments are accessed by the args
attribute on the Exception
object. For example:
try:
some_code_that_may_raise_our_value_error()
except ValueError as err:
print(err.args)
prints
('message', 'foo', 'bar', 'baz')
In Python 2.5, an actual message
attribute was added to BaseException
in favor of encouraging users to subclass Exceptions and stop using args
, but the introduction of message
and the original deprecation of args has been retracted.
Best Practices: except
clause
When inside an except clause, you might want to, for example, log that a specific type of error happened, and then re-raise. The best way to do this while preserving the stack trace is to use a bare raise statement. For example:
logger = logging.getLogger(__name__)
try:
do_something_in_app_that_breaks_easily()
except AppError as error:
logger.error(error)
raise # just this!
# raise AppError # Don't do this, you'll lose the stack trace!
Don’t modify your errors… but if you insist.
You can preserve the stacktrace (and error value) with sys.exc_info()
, but this is way more error prone and has compatibility problems between Python 2 and 3, prefer to use a bare raise
to re-raise.
To explain — the sys.exc_info()
returns the type, value, and traceback.
type, value, traceback = sys.exc_info()
This is the syntax in Python 2 — note this is not compatible with Python 3:
raise AppError, error, sys.exc_info()[2] # avoid this.
# Equivalently, as error *is* the second object:
raise sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2]
If you want to, you can modify what happens with your new raise — e.g. setting new args
for the instance:
def error():
raise ValueError('oops!')
def catch_error_modify_message():
try:
error()
except ValueError:
error_type, error_instance, traceback = sys.exc_info()
error_instance.args = (error_instance.args[0] + ' <modification>',)
raise error_type, error_instance, traceback
And we have preserved the whole traceback while modifying the args. Note that this is not a best practice and it is invalid syntax in Python 3 (making keeping compatibility much harder to work around).
>>> catch_error_modify_message()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in catch_error_modify_message
File "<stdin>", line 2, in error
ValueError: oops! <modification>
In Python 3:
raise error.with_traceback(sys.exc_info()[2])
Again: avoid manually manipulating tracebacks. It’s less efficient and more error prone. And if you’re using threading and sys.exc_info
you may even get the wrong traceback (especially if you’re using exception handling for control flow — which I’d personally tend to avoid.)
Python 3, Exception chaining
In Python 3, you can chain Exceptions, which preserve tracebacks:
raise RuntimeError('specific message') from error
Be aware:
- this does allow changing the error type raised, and
- this is not compatible with Python 2.
Deprecated Methods:
These can easily hide and even get into production code. You want to raise an exception, and doing them will raise an exception, but not the one intended!
Valid in Python 2, but not in Python 3 is the following:
raise ValueError, 'message' # Don't do this, it's deprecated!
Only valid in much older versions of Python (2.4 and lower), you may still see people raising strings:
raise 'message' # really really wrong. don't do this.
In all modern versions, this will actually raise a TypeError
, because you’re not raising a BaseException
type. If you’re not checking for the right exception and don’t have a reviewer that’s aware of the issue, it could get into production.
Example Usage
I raise Exceptions to warn consumers of my API if they’re using it incorrectly:
def api_func(foo):
'''foo should be either 'baz' or 'bar'. returns something very useful.'''
if foo not in _ALLOWED_ARGS:
raise ValueError('{foo} wrong, use "baz" or "bar"'.format(foo=repr(foo)))
Create your own error types when apropos
«I want to make an error on purpose, so that it would go into the except»
You can create your own error types, if you want to indicate something specific is wrong with your application, just subclass the appropriate point in the exception hierarchy:
class MyAppLookupError(LookupError):
'''raise this when there's a lookup error for my app'''
and usage:
if important_key not in resource_dict and not ok_to_be_missing:
raise MyAppLookupError('resource is missing, and that is not ok.')
Генерация исключений и создание своих типов исключений
Последнее обновление: 30.01.2022
Генерация исключений и оператор raise
Иногда возникает необходимость вручную сгенерировать то или иное исключение. Для этого применяется оператор raise. Например, сгенерируем исключение
try: number1 = int(input("Введите первое число: ")) number2 = int(input("Введите второе число: ")) if number2 == 0: raise Exception("Второе число не должно быть равно 0") print("Результат деления двух чисел:", number1/number2) except ValueError: print("Введены некорректные данные") except Exception as e: print(e) print("Завершение программы")
Оператору raise передается объект BaseException — в данном случае объект Exception. В конструктор
этого типа можно ему передать сообщение, которое затем можно вывести пользователю. В итоге, если number2 будет равно 0, то сработает оператор
raise, который сгенерирует исключение. В итоге управление программой перейдет к блоку except, который обрабатывает
исключения типа Exception:
Введите первое число: 1 Введите второе число: 0 Второе число не должно быть равно 0 Завершение программы
Создание своих типов исключений
В языке Python мы не ограничены только встроенными типами исключений и можем, применяя наследование, при необходимости создавать свои
типы исключений. Например, возьмем следующий класс Person:
class Person: def __init__(self, name, age): self.__name = name # устанавливаем имя self.__age = age # устанавливаем возраст def display_info(self): print(f"Имя: {self.__name} Возраст: {self.__age}")
Здесь класс Person в конструкторе получает значения для имени и возраста и присваивает их приватным переменным name и age.
Однако при создании объекта Person мы можем передать в конструктор некорректное с точки зрения логики значение — например, отрицательное число.
Одним из способов решения данной ситуации представляет генерация исключения при передаче некорректных значений.
Итак, определим следующий код программы:
class PersonAgeException(Exception): def __init__(self, age, minage, maxage): self.age = age self.minage = minage self.maxage = maxage def __str__(self): return f"Недопустимое значение: {self.age}. " f"Возраст должен быть в диапазоне от {self.minage} до {self.maxage}" class Person: def __init__(self, name, age): self.__name = name # устанавливаем имя minage, maxage = 1, 110 if minage < age < maxage: # устанавливаем возраст, если передано корректное значение self.__age = age else: # иначе генерируем исключение raise PersonAgeException(age, minage, maxage) def display_info(self): print(f"Имя: {self.__name} Возраст: {self.__age}") try: tom = Person("Tom", 37) tom.display_info() # Имя: Tom Возраст: 37 bob = Person("Bob", -23) bob.display_info() except PersonAgeException as e: print(e) # Недопустимое значение: -23. Возраст должен быть в диапазоне от 1 до 110
В начале здесь определен класс PersonAgeException, который наследуется от класса Exception. Как правило, собственные классы исключений
наследуются от класса Exception. Класс PersonAgeException предназначен для исключений, связанных с возрастом пользователя.
В конструкторе PersonAgeException получаем три значения — собственное некорректное значение, которое послужило причиной исключения,
а также минимальное и максимальное значения возраста.
class PersonAgeException(Exception): def __init__(self, age, minage, maxage): self.age = age self.minage = minage self.maxage = maxage def __str__(self): return f"Недопустимое значение: {self.age}. " f"Возраст должен быть в диапазоне от {self.minage} до {self.maxage}"
В функции __str__ определяем текстовое представление класса — по сути сообщение об ошибке.
В конструкторе класса Persoon проверяем переданное для возраста пользователя значение. И если это значение не соответствует
определенному диапазону, то генерируем исключение типа PersonAgeException:
raise PersonAgeException(age, minage, maxage)
При применении класса Person нам следует учитывать, что конструктор класса может сгенерировать исключение при передаче некорректного
значения. Поэтому создание объектов Person обертывается в конструкцию try..except:
try: tom = Person("Tom", 37) tom.display_info() # Имя: Tom Возраст: 37 bob = Person("Bob", -23) # генерируется исключение типа PersonAgeException bob.display_info() except PersonAgeException as e: print(e) # Недопустимое значение: -23. Возраст должен быть в диапазоне от 1 до 110
И если при вызове конструктора Person будет сгенерировано исключение типа PersonAgeException, то управление программой перейдет к
блоку except, который обрабатывает исключения типа PersonAgeException в виде вывода информации об исключении на консоль.
В Python пользователи могут определять свои собственные исключения, создавая новый класс. Этот класс исключений должен прямо или косвенно быть производным от встроенного класса Exception
. Большинство встроенных исключений также являются производными от этого класса. Все пользовательские исключения также должны быть производными от этого класса.
Пользовательские исключения полезны тем, что их можно вызвать с неправильными или неожиданными входными данными, тем самым лучше прояснив ситуацию с кодом, который падает или неправильно работает.
В примере создается определяемое пользователем исключение CustomError()
, которое наследуется от класса Exception
. Это новое исключение, как и другие исключения, может быть вызвано с помощью оператора raise
с дополнительным сообщением об ошибке.
# Определяем собственное исключение >>> class CustomError(Exception): ... pass ... >>> raise CustomError # Traceback (most recent call last): # ... # __main__.CustomError # Вызываем собственное исключение # 'CustomError' с сообщением об ошибке >>> raise CustomError("An error occurred") # Traceback (most recent call last): # ... # __main__.CustomError: An error occurred
При разработке программы на Python, хорошей практикой считается помещать все определяемые пользователем исключения в отдельный файл. Многие стандартные модули определяют свои исключения отдельно как exceptions.py
или errors.py
(обычно, но не всегда).
Пользовательский класс исключений может реализовать все, что может делать обычный класс, но обычно их делают простыми и краткими. Большинство реализаций пользовательских исключений объявляют настраиваемый базовый класс и наследуют другие классы исключений из этого базового класса.
Большинство пользовательских исключений определяются именами, которые заканчиваются на «Error», аналогично именованию стандартных исключений.
В следующем примере иллюстрируется, как пользовательские исключения могут использоваться в программе для создания и перехвата ошибок.
Программа просит ввести число до тех пор, пока оно не будет равно загаданному. В качестве подсказки, пользователю каждый раз выводятся сообщения, о том, больше или меньше введенное число чем загаданное.
# определение пользовательских исключений class Error(Exception): """Базовый класс для других исключений""" pass class ValueTooSmallError(Error): """Вызывается, когда входное значение мало""" pass class ValueTooLargeError(Error): """Вызывается, когда входное значение велико""" pass # число, которое нужно угадать number = 10 # игра продолжается до тех пор, # пока пользователь его не угадает while True: try: i_num = int(input("Ввести число: ")) if i_num < number: raise ValueTooSmallError elif i_num > number: raise ValueTooLargeError break except ValueTooSmallError: print("Это число меньше загаданного, попробуйте еще раз!n") except ValueTooLargeError: print("Это число больше загаданного, попробуйте еще раз!n") print("Поздравляю! Вы правильно угадали.")
В примере определен базовый класс под названием Error()
. Два других исключения, которые фактически вызываются программой (ValueTooSmallError
и ValueTooLargeError
), являются производными от класса Error()
.
Это стандартный способ определения пользовательских исключений в программировании на Python, но ни кто не ограничен только этим способом.
Пример запуска скрипта с примером:
Ввести число: 12 Это число больше загаданного, попробуйте еще раз! Ввести число: 0 Это число меньше загаданного, попробуйте еще раз! Ввести число: 8 Это число меньше загаданного, попробуйте еще раз! Ввести число: 10 Поздравляю! Вы правильно угадали.
Смотрим еще один простенький пример.
Пользовательские классы исключений, часто предлагая только ряд атрибутов, которые позволяют извлекать информацию об ошибке для исключения.
class Error(Exception): """Базовый класс для исключений в этом модуле.""" pass class InputError(Error): """Исключение для ошибок во входных данных. Attributes: expression -- выражение, в котором произошла ошибка message -- объяснение ошибки """ def __init__(self, expression, message): self.expression = expression self.message = message x = input("Ведите положительное целое число: ") try: x = int(x) if x < 0: raise InputError(f'!!! x = input({x})', '-> Допустимы только положительные числа.') except ValueError: print("Error type of value!") except InputError as e: print(e.args[0]) print(e.args[1]) else: print(x) # Ведите положительное целое число: 3 # 3 # Ведите положительное целое число: 7.9 # Error type of value! # Ведите положительное целое число: -5 # !!! x = input(-5) # -> Допустимы только положительные числа.
У объектов класса исключений Exception
и его производных, определен метод __str__()
так, чтобы выводить значения атрибутов. Поэтому можно не обращаться напрямую к полям объекта: e.expression
и e.message
. Кроме того у экземпляров класса исключений Exception
есть атрибут args
. Через него можно получать доступ к отдельным полям, как показано в примере выше.
Многие стандартные модули определяют свои собственные исключения для сообщений об ошибках, которые могут возникать в определяемых ими функциях. Более подробная информация о классах представлена в разделе «Классы в Python».
Настройка собственных классов исключений.
Для тонкой настройки своего класса исключения нужно иметь базовые знания объектно-ориентированного программирования.
Чтобы принимать дополнительные аргументы в соответствии с задачами конкретного исключения, необходимо дополнительно настроить пользовательский класс исключения.
Рассмотрим пример:
class SalaryNotInRangeError(Exception): """Исключение возникает из-за ошибок в зарплате. Атрибуты: salary: входная зарплата, вызвавшая ошибку message: объяснение ошибки """ def __init__(self, salary, message="Зарплата не входит в диапазон (5000, 15000)"): self.salary = salary self.message = message # переопределяется конструктор встроенного класса `Exception()` super().__init__(self.message) salary = int(input("Введите сумму зарплаты: ")) if not 5000 < salary < 15000: raise SalaryNotInRangeError(salary)
В примере, для приема аргументов salary
и message
переопределяется конструктор встроенного класса Exception()
. Затем конструктор родительского класса Exception()
вызывается вручную с аргументом self.message
при помощи функции super()
. Пользовательский атрибут self.salary
определен для использования позже.
Результаты запуска скрипта:
Введите сумму зарплаты: 2000 Traceback (most recent call last): File "test.py", line 17, in <module> raise SalaryNotInRangeError(salary) __main__.SalaryNotInRangeError: Зарплата не входит в диапазон (5000, 15000)
Унаследованный метод __str__
класса Exception()
используется для отображения соответствующего сообщения при возникновении SalaryNotInRangeError()
. Также можно настроить сам метод __str__
, переопределив его.
class SalaryNotInRangeError(Exception): """Исключение возникает из-за ошибок в зарплате. Атрибуты: salary: входная зарплата, вызвавшая ошибку message: объяснение ошибки """ def __init__(self, salary, message="Зарплата не входит в диапазон (5000, 15000)"): self.salary = salary self.message = message super().__init__(self.message) # переопределяем метод '__str__' def __str__(self): return f'{self.salary} -> {self.message}' salary = int(input("Введите сумму зарплаты: ")) if not 5000 < salary < 15000: raise SalaryNotInRangeError(salary)
Вывод работы скрипта:
Введите сумму зарплаты: 2000 Traceback (most recent call last): File "test.py", line 20, in <module> raise SalaryNotInRangeError(salary) __main__.SalaryNotInRangeError: 2000 -> Зарплата не входит в диапазон (5000, 15000)
Как перехватывать пользовательское исключение.
Определение собственного исключения не меняет того, какие исключения создаются используемым, в данный момент модулем.
Если необходимо, чтобы код использовал пользовательское исключение, то сначала нужно перехватить исключение, определяемое используемым модулем, а затем повторно вызвать, при помощи raise
, своё собственное исключение.
import sqlite3 class MyError(Exception): """Could not connect to db""" pass try: conn= sqlite3.connect('database.sqlite') except sqlite3.Error as e: raise MyError(f'Could not connect to db: {e.value}')
В примере ловиться исключение, определяемое модулем sqlite3
, а затем вызывается пользовательское исключение при помощи raise
.