Исключения
Contents
Исключения#
В Python
существует минимум два типа ошибок: синтаксические ошибки и исключения. Синтаксические ошибки чаще возникают у изучающих python
программистов из-за нарушения синтаксиса. Но попытка запуска корректной с точки зрения синтаксиса программы может привести к возникновению ошибки. Ошибки, обнаруженные во время исполнения программы, называются исключениями (exception
) и не всегда ведут к падению программы: их можно обрабатывать и восстанавливать нормальный поток исполнения программы.
Тем не менее большинство ошибок программой не обрабатывается и их возникновение приводит к выводу сообщения об ошибке. В качестве примера попробуем поделить на ноль.
1/0
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_13252/2354412189.py in <module>
----> 1 1/0
ZeroDivisionError: division by zero
Как видимо, появилось сообщение об ошибке. Последняя строка этого сообщения, говорит, что конкретно произошло.
Исключения бывают разных типов и конкретный тип брошенного исключения печатается до двоеточия. В данном случае это ZeroDivisionError.
После типа ошибки выводится дополнительная информация, которая может зависеть от типа ошибки и причины её возникновения.
Всё, что идёт до последней строки, демонстрирует в каком контексте возникла ошибка в виде трассировочных данных. Более наглядное представление о трассировочной информации даёт пример, когда ошибка возникает внутри нескольких функций.
def f():
1/0
def g():
f()
g()
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_13252/4200539898.py in <module>
5 f()
6
----> 7 g()
~\AppData\Local\Temp/ipykernel_13252/4200539898.py in g()
3
4 def g():
----> 5 f()
6
7 g()
~\AppData\Local\Temp/ipykernel_13252/4200539898.py in f()
1 def f():
----> 2 1/0
3
4 def g():
5 f()
ZeroDivisionError: division by zero
В этот раз сообщение об ошибке гораздо длиннее, потому что ошибка возникла не на самом верхнем уровне, а на уровне функции.
Встроенные исключения#
В python
есть приличное количество встроенных исключений, которые интерпретатор бросает при обнаружении ошибки во время исполнения. Например, если попытаться обратиться к необъявленной переменной (несуществующему имени), то возникнет ошибка NameError. Обращение к неправильному атрибуту вызовет ошибку AttributeError, обращение за пределы списка — IndexError, поиск в словаре по несуществующему ключу — KeyError и т.д.
В ряде ситуаций python
возбуждает исключение не в результате ошибки, а в служебных целях. Например, как обсуждалось ранее, функции работающие с итерируемыми объектами ожидают исключение StopIteration, т.е. появление такого исключения — не является исключительной ситуацией, связанной с логической ошибкой в коде, а лишь механизм, которым итерируемый объект сообщает клиентскому коду, что элементы в нем исчерпались.
В python
всё является объектом, в том числе и исключения. При этом у каждого исключения есть свой тип, примеры которых уже обсуждалось выше: NameError
, AttributeError
, StopIteration
и т.д. — это всё тип исключения. Обнаружение ошибки приводит к тому, что создаётся экземпляр исключения соответствующего типа и это исключение начинает распространяться. При этом все встроенные типы исключений находятся в иерархии, с которой можно ознакомиться по ссылке.
Так, на самом верху располагается базовый класс BaseException от которого наследуется все остальные типы исключений. На втором этаже располагаются исключения связанные с завершением исполнения программы (например, SystemExit возникает при вызове функции sys.exit(), KeyboardInterrupt возникает, если пользователь нажимает прерывающую комбинацию клавиш во время работы приложения), а так же класс Exception, который и является базовым классом для всех исключений не связанных с завершением программы.
С полным списком встроенных исключений можно ознакомиться на странице официальной документации.
Возбуждение исключений. raise
#
Многие исключения бросаются интерпретатором python
при обнаружении ошибки или в служебных целях. Для этого используется ключевое слово raise
справа от которого указывается экземпляр исключения.
raise Exception("Моё первое исключение!")
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_13252/2359356075.py in <module>
----> 1 raise Exception("Моё первое исключение!")
Exception: Моё первое исключение!
Когда исключение возбужденно, оно начинает распространяться, т.е. нормальное исполнение программы прекращается. Так, например, покинуть функцию можно не только используя ключевое слово return
, но и ключевым словом raise
.
def f():
print("До исключения!")
raise Exception("Исключение!")
print("После исключения!")
f()
До исключения!
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_13252/2985915162.py in <module>
4 print("После исключения!")
5
----> 6 f()
~\AppData\Local\Temp/ipykernel_13252/2985915162.py in f()
1 def f():
2 print("До исключения!")
----> 3 raise Exception("Исключение!")
4 print("После исключения!")
5
Exception: Исключение!
Видим, что инструкция print("До исключения!")
исполнилась, а инструкция print("После исключения")
— нет, т.к. сразу после возбуждения исключения нормальный поток исполнения программы прекратился и исключение начало распространяться. Исключение распространяется наверх по стеку вызовов, пока не встретится блок, обрабатывающий это исключение. Если такой блок так и не встречается, то выводится сообщение об ошибке и программа падает (или может продолжить ожидание инструкций от пользователя в интерактивном режиме, что происходит, например, в jupyter
ноутбуках).
Обработка исключений.#
Обработка ошибок в python
осуществляется с помощью инструкции try
, которая может встречаться в блоках двух видов:
try/except
;try/finally
;
Блок try/except
#
С помощью блока try/except
можно обрабатывать конкретные типы исключений. В самой простой своей форме он выглядит примерно следующим образом.
try:
инструкции
except ТипИсключения:
инструкции
После ключевого слова try
помещаются инструкции, исполнение которых может привести к возникновению исключения. Инструкции в блоке except
выполняются только, если а) вовремя выполнения инструкций в блоке try
возникла ошибка и б) её тип совпал с указанным типом.
В качестве примера опять поделим на ноль, но в этот раз поместим это деление в блок try/except
и обработаем исключение типа ZeroDivisionError
.
try:
1/0
print("После попытки деления.")
except ZeroDivisionError:
print("Перехвачена ошибка деления на ноль!")
Перехвачена ошибка деления на ноль!
Видим, что
сообщения об ошибке с типом ошибки и трассировочной информацией не появилось, т.к. ошибка была обработана;
вместо этого выполнилась инструкция
print
в блокеexcept ZeroDivisionError
;инструкция
print
в блокеtry
после деления на ноль не выполнилась, т.к. ошибка возникла раньше и начала распространяться.
Когда исключение возникает, оно начинает распространяться вверх по стеку вызовов функций, а нормальное исполнение программы прекращается. Как только встречается первый блок except
совпадающего типа, распространение исключения прекращается и выполняется код в этом блоке.
Приведем несколько примеров, чтобы продемонстрировать этот механизм.
Следующий пример демонстрирует, что исключение продолжает распространяться, пока не встретит правильный блок except
.
try:
try:
1/0
except NameError:
print("Ошибка перехвачена внутренним блоком try/except.")
except ZeroDivisionError:
print("Ошибка перехвачена внешним блоком try/except.")
print("Инструкция после обработки исключения.")
Ошибка перехвачена внешним блоком try/except.
Инструкция после обработки исключения.
Здесь исключение ZeroDivisionError
возникло во внутреннем блоке try/except
, но тип возникшего исключения не совпал с NameError
. В итоге внутренний блок try/except
ошибку не обработало и она продолжила распространяться, в результате чего попала во внешний блок try/except
, который обрабатывает ошибки типа ZeroDivisionError
, что привело прекращению распространения ошибки и к исполнению инструкции print
во внешнем блоке. Далее программа продолжает исполняться в нормальном режиме.
Следующий пример демонстрирует, что исключение прекращает распространяться, как только встретит правильный блок except
.
try:
try:
1/0
except ZeroDivisionError:
print("Ошибка перехвачена внутренним блоком except.")
except ZeroDivisionError:
print("Ошибка перехвачена внешним блоком except.")
print("Инструкция после обработки исключения.")
Ошибка перехвачена внутренним блоком except.
Инструкция после обработки исключения.
Здесь внутренний блок try/except
обрабатывает правильный тип исключения. Возникшее исключение прекращает распространяться в этом внутреннем блоке try/except
и выполняется инструкция print
во внутреннем блоке except
. Т.к. исполнение инструкций после внешнего ключевого слова try
проходит без ошибок (ошибка возникает, но внешний блок её не замечает, т.к. она обрабатывается внутренним блоком try/except
), то внешний блок except
игнорируется.
Блоков except
может быть несколько в одном блоке try/except
, что позволяет по-разному реагировать на ошибки разных типов.
try:
1/0
except NameError:
print("Перехвачена ошибка NameError")
except ZeroDivisionError:
print("Перехвачена ошибка ZeroDivisionError")
Перехвачена ошибка ZeroDivisionError
Интерпретатор читает блоки except
сверху вниз и останавливается на первом, который обрабатывает исключение правильного типа. Т.к. типы исключений располагаются в иерархии между собой, то необходимо придерживаться следующего правила.
Tip
Сначала всегда обрабатывайте самые специфичные исключения, а уже потом более общие.
Если не придерживаться этого правила, то специфичные блоки except
будут недостижимы. Продемонстрируем это на примере, воспользовавшись тем, что Exception
является базовым классом почти для всех остальных встроенных исключений, а значит исключения этого типа являются чуть ли не самыми общими.
try:
1/0
except Exception:
print("Перехвачено неожиданное исключение!")
except ZeroDivisionError:
print("Перехвачено исключение ZeroDivisionError!")
Перехвачено неожиданное исключение!
Здесь в блоке try
возникает исключение ZeroDivisionError
и начинает распространяться. В блоке except Exception
проверка на тип успешно проходит (иерархия между ними выглядит так: Exception
\(\to\) ArithmeticError
\(\to\) ZeroDivisionError
), а значит вызывается инструкции в соответствующем блоке, а до блока except ZeroDivisionError
дело так и не доходит: ошибка обработана ранее.
Правильнее в данном случае было бы поменять эти блоки местами.
try:
1/0
except ZeroDivisionError:
print("Перехвачено исключение ZeroDivisionError!")
except Exception:
print("Перехвачено неожиданное исключение!")
Перехвачено исключение ZeroDivisionError!
Дополнительные возможности except
#
После except
можно указать целевую переменную, чтобы получить доступ к объекту исключения синтаксисом
except ТипИсключения as цель:
операции над цель
Из некоторых объектов исключений можно получить дополнительную информацию об контексте, при которым они возбудились. У всех стандартных исключений преобразование к строке определенно таким образом, чтобы выводить сообщение, указанное при создании.
try:
raise Exception("Сообщение исключения!")
except Exception as e:
print(e)
Сообщение исключения!
После ключевого слова except
необязательно должен быть указан только один тип исключения: можно указать кортеж.
try:
1/0
except (ZeroDivisionError, NameError):
print("Возникло исключение ZeroDivisionError или NameError!")
Возникло исключение ZeroDivisionError или NameError!
Так же, в самом конце блока try/except
можно добавить блок else
, инструкции в котором исполняться, только если блок try
завершится без ошибок. Чтобы продемонстрировать это, определим функцию divide
, которая будет пытаться делить первый аргумент на второй и возвращать результат деления, если деление произошло успешно, и возвращать бесконечность, если второй параметр равен нулю.
def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Попытка деления на ноль.")
return float("inf")
else:
print("Деление произошло успешно.")
return result
Блок else
в этой функции исполнится, только если в блоке try
деление произойдет успешно, т.е. если не возникнет ошибки деления на ноль.
print(divide(42, 14))
print(divide(42, 0))
Деление произошло успешно.
3.0
Попытка деления на ноль.
inf
Блок try/finally
#
Синтаксис блока try/except
выглядит следующим образом.
try:
инструкции
finally:
инструкции
Инструкции в блоке finally
исполнятся в не зависимости от того, возникнет ли ошибка в блоке try
или нет. Если ошибка все же возникает, то выполняется код в блоке finally
и ошибка продолжает распространяться.
Этот блок часто называют обработчик очистки: часто в нем размещают инструкции, которые выполняют чистку (например, освобождение ресурсов). Это позволяет гарантировать, что инструкции в этом блоке исполнятся в любом случае.
Например, раньше часто встречались конструкции следующего вида.
f = open(some_file, "w")
try:
do_something_with_file(f)
finally:
f.close()
Т.е. операции над открытым файлом производились в блоке try/finally
, что гарантировало, что файл будет закрыт, даже если возникнет исключение. Сегодня гораздо удобнее использовать контекстные менеджеры with
для таких целей.
Блок try/except/finally
#
Если встречается блок обработки ошибки, в котором встречаются и except
и finally
, то его можно представить в виде вложения блока try/except
внутрь блока try/finally
. Иными словами, следующие две конструкции ведут себя одинаково.
try:
инструкции
except SomeException:
инструкции
else:
инструкции
finally:
инструкции
try:
try:
инструкции
except SomeException:
инструкции
else:
инструкции
finally:
инструкции
Для демонстрации работы блока такого вида приведем пример из документации.
def divide(x, y):
try:
result = x / y
except ZeroDivisionError:
print("Попытка деления на ноль!")
else:
print(f"Результат деления {result}")
finally:
print("Выполнение блока 'finally'.")
Функция divide
пытается поделить x
на y
.
Если деление происходит успешно (else
), то выводится результат.
divide(2, 1)
Результат деления 2.0
Выполнение блока 'finally'.
Если возникает ошибка деления на ноль, то она перехватывается и выводится соответствующее сообщение.
divide(2, 0)
Попытка деления на ноль!
Выполнение блока 'finally'.
Если возникает ошибка другого рода, то её распространение продолжается.
divide("2", "1")
executing finally clause
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_13252/1759864827.py in <module>
----> 1 divide("2", "1")
~\AppData\Local\Temp/ipykernel_13252/3246861353.py in divide(x, y)
1 def divide(x, y):
2 try:
----> 3 result = x / y
4 except ZeroDivisionError:
5 print("division by zero!")
TypeError: unsupported operand type(s) for /: 'str' and 'str'
Блок finally
выполняется в любом случае.