Поговорим об исключениях.
Всё нижеизложенное относится к Python 3.3, хотя отчасти справедливо и для более ранних версий.
Для чего они применяются, наверное, все и так прекрасно знают: для передачи сообщений об ошибках внутри программы.
Рассмотрим простейший пример: открытие файла. Если всё нормально — open(filename, 'r') возвращает объект этого самого файла, с которым можно делать всякие полезные вещи: читать из него данные и т.д.
Если файл не может быть открыт — выбрасывается исключение:
try:
f = open(filename, 'r')
try:
print(f.read())
finally:
f.close()
except OSError as ex:
print("Cannot process file", filename, ": Error is", ex)
Открываем файл и печатаем его содержимое.
Обратите внимание: файл нужно не только открыть но и закрыть после использования. Исключение может выбросить open (например, если файла нет на диске или нет прав на его чтение).
Если файл открыт — читаем его через f.read(). Этот вызов тоже может выбросить исключение, но файл закрывать всё равно нужно. Поэтому необходим блок finally: f.close() должен быть вызван даже если f.read() сломался. В этом месте удобней было бы воспользоваться конструкцией with но мы же сейчас говорим об исключениях а не о контекстных менеджерах, верно?
Исключения из обоих мест попадут в except OSError, где можно будет что-то сделать с ошибкой.
Питон делает явный выбор в пользу исключений перед возвратом кода ошибки в своём ядре и стандартной библиотеке. Прикладному программисту стоит придерживаться этих же правил.
Введение закончено. Теперь сконцентрируемся на том что происходит в except.
Типы исключений
Все исключения составляют иерархию с простым наследованием. Вот простой небольшой кусочек от довольно обширного набора исключений ядра Питона:
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- AssertionError
+-- AttributeError
+-- BufferError
Самый базовый класс — BaseException. Он и его простые потомки (SystemExit, KeyboardInterrupt, GeneratorExit) не предназначены для перехвата обыкновенным программистом — только Питон и редкие библиотеки должны работать с этими типами. Нарушение правила ведет, например, к тому что программу невозможно корректно завершить — что совсем не хорошо.
Также не нужно перехватывать все исключения:
try:
...
except:
...
работает как
try:
...
except BaseException:
...
Всё, что может быть нужно программисту — это Exception и унаследованные от него классы.
Вообще-то лучше ловить как можно более конкретные классы исключений. Например, в случае с файлом это OSError или даже может быть FileNotFoundError. Таким образом мы не перехватим AttributeErrorили ValueError, которые в этом примере означали бы ошибку или опечатку программиста.
Кстати, обратите внимание: StopIteration порожден от Exception а GeneratorExit от BaseException. Подробности, почему сделано именно так, можно найти в PEP 342.
Цепочки исключений
Прочитав предыдущую главку все прониклись необходимостью указывать правильный класс исключений и пообещали никогда не использовать BaseException.
Идем дальше. Следующий пример:
try:
user = get_user_from_db(login)
except DBError as ex:
raise UserNotFoundError(login) from ex
Получаем пользователя из базы данных чтобы что-то потом с ним сделать. get_user_from_db может выбросить ошибку базы данных. Для нас это скорее всего означает что такого пользователя нет. Но для логики приложения полезней наш собственный тип UserNotFoundError с указанием логина проблемного пользователя, а не обезличенная ошибка БД — что мы и выбрасываем в обработчике исключения.
Проблема в том, что программисту часто хотелось бы знать, а почему это пользователь не найден. Например, чтобы сохранить в логах для дальнейшего разбирательства.
Для таких целей служит конструкция raise ... from ....
По PEP 3134 у объекта исключения имеется несколько обязательных атрибутов.
В первую очередь это __traceback__, содержащий кусочек стека от места возникновения исключения до места его обработки.
Затем — __context__. Если исключение было создано в ходе обработки другого исключения (выброшено из except блока) — __context__будет содержать то самое породившее исключение. Которое, в свою очередь тоже может иметь установленный __context__. Этот атрибут равен None если наше исключение — самое первое и н
е имеет предшественников.
__context__ устанавливается автоматически.
В отличие от контекста __cause__ устанавливается только если исключение было выброшено конструкцией raise ... from ... и равно значению from.
Если исключение выбрасывалось простым raise ... то __cause__ будет равно None в то время как __context__ всегда будет содержать породившее исключение если оно существует.
Для вывода исключения со всей информацией служит набор функций из модуля traceback, например traceback.print_exc().
И тут тоже есть проблема: печатается либо явная цепочка если есть установленный __cause__ или неявная, тогда используется __context__.
Иногда программисту может быть нужно отбросить породившие исключения как не имеющие смысла при выводе traceback. Для этого появилась форма записи
raise exc from None
PEP 409 и PEP 415 рассказывают как это работает:
У исключения всегда есть атрибут __supress_context__. По умолчанию он равен False.
Конструкция raise ... from ... записывает fromв __cause__ и устанавливает __supress_context__ в True.
Тогда семейство функций traceback.print_exc() печатают цепочку если явно указан (не равен None) __cause__ или есть __context__ и при этом __supress_context__ равен False.
Изложение получилось несколько длинным, но сократить текст без потери смысла у меня не вышло.
Семейство OSError
Последняя проблема о которой хотелось бы рассказать — это типы исключений порожденные вызовами операционной системы.
До Python 3.3 существовало много разных типов таких исключений: os.error, socket.error, IOError, WindowsError, select.errorи т.д.
Это приводило к тому, что приходилось указывать несколько типов обрабатываемых исключений одновременно:
try:
do_something()
except (os.error, IOError) as ex:
pass
Ситуация на самом деле была еще хуже: очень легко забыть указать еще одно нужное исключение, которое может внезапно прилететь. Дело в том что исключения операционной системы часто никак не проявляют себя при разработке. На машине программиста всё работает отлично и он не подозревает о возможных проблемах. Как только программа выходит в production пользователь вдруг ловит что-то неожиданное и программа аварийно завершается. Все опечалены.
Проблема решена в PEP 3151: весь этот зоопарк теперь является псевдонимами для OSError. Т.е. пишите OSError и не ошибетесь (прочие имена оставлены для обратной совместимости и облегчения портирования кода на новую версию).
Давайте рассмотрим ещё один аспект исключений, порожденных операционной системой.
У OSError есть атрибут errno, который содержит код ошибки (список всех возможных символьных констант для ошибок можно посмотреть в модуле errno).
Открываем файл, получаем OSError в ответ. Раньше мы должны были анализировать ex.errno чтобы понять, отчего произошла ошибка: может файла нет на диске, а может нет прав на запись — это разные коды ошибок (ENOENT если файла нет и EACCES или EPERM если нет прав).
Приходилось строить конструкцию вроде следующей:
try:
f = open(filename)
except OSError as ex:
if ex.errno == errno.ENOENT:
handle_file_not_found(filename)
elif ex.errno in (errno.EACCES, errno.EPERM):
handle_no_perm(filename)
else:
raise # обязательно выбрасывать не обработанные коды ошибки
Теперь иерархия расширилась. Привожу полный список наследников OSError:
OSError
+-- BlockingIOError
+-- ChildProcessError
+-- ConnectionError
| +-- BrokenPipeError
| +-- ConnectionAbortedError
| +-- ConnectionRefusedError
| +-- ConnectionResetError
+-- FileExistsError
+-- FileNotFoundError
+-- InterruptedError
+-- IsADirectoryError
+-- NotADirectoryError
+-- PermissionError
+-- ProcessLookupError
+-- TimeoutError
Наш пример можем переписать как:
try:
f = open(filename)
except FileNotFound as ex:
handle_file_not_found(filename)
except PermissionError as ex:
handle_no_perm(filename)
Гораздо проще и понятней, правда? И меньше мест, где программист может ошибиться.
Заключение
Переходите на Python 3.3, если можете. Он хороший и облегчает жизнь.
Новые плюшки в вопросе, касающемся исключений, я показал.
Если использовать новый питон не позволяют обстоятельства — пишите на чём есть, но помните как правильно это делать.
Автор: Andrew Svetlov