Модуль 8 · Исключения и файлы

Справочник по Python · модуль 8 · исключения и файлы

Глава 8.3

8.3

Иерархия
исключений

Встроенных исключений больше шестидесяти, а со стандартной библиотекой — несколько сотен. Запоминать их по одному бессмысленно: они объединены в дерево наследования, и перехват родителя ловит всех потомков. Знание нескольких узлов этого дерева заменяет знание сотен имён.

Категория
Обработка ошибок
Уровень
Средний
Связанные темы
Наследование, типы ошибок, отладка
Зачем это знать
Выбирать правильную широту перехвата

§1Проблема количества

Обработка исключений кажется хаотичной, и на то есть причины. Только встроенных исключений больше шестидесяти. Заранее определить, какие из них возможны в конкретном месте, обычно нельзя: типы исключений не указываются в сигнатуре функции, и никакой инструмент не проверит, что вы обработали всё нужное.

Выход не в том, чтобы выучить список, а в том, чтобы понять его устройство. Исключения — это классы, связанные наследованием, и перехват родителя ловит всех его потомков. Значит, вместо десятка конкретных типов часто достаточно одной категории.

§2Родитель ловит потомков

Задача: достать элемент из контейнера, который может оказаться и списком, и словарём. Ошибки будут разные — IndexError или KeyError.

main.pyдва обработчика для одной задачи
try:
    item = items[index]
except IndexError:
    # выдаётся, если items — последовательность
    ...
except KeyError:
    # выдаётся, если items — отображение
    ...

Оба типа наследуют от общего родителя, поэтому запись сокращается вдвое.

main.pyодин обработчик вместо двух
try:
    item = items[index]
except LookupError:
    ...

LookupError — категория «ошибка выборки из контейнера». Она достаточно широка, чтобы покрыть оба случая, и достаточно узка, чтобы не поймать что-то постороннее.

§3Пример: LookupError

main.pyобе ошибки перехвачены одинаково
values = [1, 2]
mapping = {"a": 1}

for get in (lambda: values[10], lambda: mapping["z"]):
    try:
        get()
    except LookupError as e:
        print(type(e).__name__, "-", e)
Выводтип разный, обработчик один
IndexError - list index out of range
KeyError - 'z'

Запись type(e).__name__ показывает настоящий тип пойманного исключения — родитель ловит, но не подменяет.

§4Общее дерево

Схемадерево наследования исключений
BaseException Exception SystemExit GeneratorExit KeyboardInterrupt AttributeError ArithmeticError EOFError NameError LookupError OSError TypeError ValueError ZeroDivisionError FloatingPointError OverflowError IndexError KeyError FileNotFoundError PermissionError TimeoutError
  • корень и обычная граница перехвата
  • категории и конкретные ошибки
  • служебные: перехватывать не следует

Восемь узлов сверху — это почти всё, что нужно помнить. Остальные сотни исключений висят на них листьями.

§5BaseException и Exception

Корень дерева — BaseException. От него наследует всё, но напрямую — только три особых исключения, которые управляют ходом программы, а не сообщают об ошибке.

КлассЧто означаетЛовить?
BaseExceptionкорень всей иерархиипрактически никогда
SystemExitнамеренное завершение программынет
KeyboardInterruptпользователь нажал Ctrl+Cнет
Exceptionвсе обычные ошибкида, если нужен общий перехват

Отсюда практическое правило: ловят Exception, а не BaseException. Все ошибки, относящиеся к работе программы, наследуют от Exception.

§6Почему не except BaseException

Разница на первый взгляд формальная, но последствия ощутимые.

main.pyCtrl+C не является ошибкой программы
print(issubclass(KeyboardInterrupt, Exception))
print(issubclass(KeyboardInterrupt, BaseException))
Выводнаследует от корня, но не от Exception
False
True

Если написать except BaseException в длинном цикле, программа перестанет реагировать на Ctrl+C: нажатие превратится в перехваченное исключение, и цикл продолжит работу. То же с SystemExit — программа не сможет завершиться. Именно поэтому голый except: из главы 8.1 плох: он равнозначен except BaseException.

§7ArithmeticError

Категория математических ошибок: ZeroDivisionError, OverflowError, FloatingPointError.

main.pyделение на ноль как частный случай
try:
    1 / 0
except ArithmeticError as e:
    print(type(e).__name__)
Выводпоймано по родителю
ZeroDivisionError

§8LookupError

Категория ошибок выборки из контейнера — та самая из §2. Потомков всего два, зато встречаются они постоянно.

ИсключениеКогда
IndexErrorиндекс за границами списка, кортежа, строки
KeyErrorключа нет в словаре или множестве

Категория намеренно узкая: она не включает ошибки, не связанные с выборкой значений. Это и делает её удобной — перехват не поймает лишнего.

§9OSError

Самая широкая из категорий: всё, что приходит от операционной системы, — файлы, сетевые подключения, права доступа, каналы, тайм-ауты.

main.pyфайла не существует
try:
    open("нет-такого.txt")
except OSError as e:
    print(type(e).__name__)
    print(e.errno, e.filename)
Выводу OSError есть дополнительные атрибуты
FileNotFoundError
2 нет-такого.txt

Это то самое исключение, о котором говорилось в главе 8.2: его args содержат не только текст. Атрибуты errno и filename позволяют разобраться, что именно не получилось. Работа с файлами начинается со следующей главы, и OSError там будет постоянным спутником. Старое имя EnvironmentError — синоним OSError.

§10ValueError и UnicodeError

ValueError означает «тип аргумента верный, но само значение недопустимо» — с ним встречались, начиная с главы 1.5.

main.pyцепочка наследования
print(issubclass(UnicodeError, ValueError))
print(issubclass(UnicodeDecodeError, UnicodeError))
Выводтри уровня вложенности
True
True

UnicodeError объединяет ошибки кодирования и декодирования из главы 5.4 — включая UnicodeDecodeError, который возникал при чтении байтов не той кодировкой. То, что он наследует от ValueError, логично: байты имеют верный тип, но недопустимы для выбранной кодировки.

§11Одиночные исключения

Часть распространённых исключений наследует прямо от Exception и в крупные группы не входит.

ИсключениеКогда возникает
TypeErrorоперация применена к объекту неверного типа
NameErrorимя не найдено в пространстве имён
UnboundLocalErrorлокальная переменная использована до присваивания
AttributeErrorу объекта нет такого атрибута или метода
RuntimeErrorобобщённое «произошло что-то плохое»
NotImplementedErrorвозможность не реализована
AssertionErrorнеудачная проверка assert
MemoryErrorнехватка памяти
EOFErrorконец файла при чтении

Почти все они встречались в предыдущих модулях: AttributeError — при вызове append() у кортежа, NameError — при обращении к переменной включения из главы 4.10.

§12Исключения потока выполнения

Три исключения используются не для ошибок, а для управления ходом программы.

ИсключениеНазначениеНаследует от
SystemExitзавершить программуBaseException
KeyboardInterruptпрерывание с клавиатурыBaseException
StopIterationперебор закончилсяException

SystemExit принимает код завершения или строку: строка выводится в поток ошибок, и программа завершается с кодом 1. KeyboardInterrupt необычно тем, что возникает асинхронно — почти в любой момент и на любой строке. StopIteration — часть протокола итераций из главы 7.1; обратите внимание, что он единственный из трёх наследует от Exception, потому что возникает в обычном ходе работы.

§13Как узнать предков

Запоминать дерево целиком не нужно — его можно спросить у самого Python.

main.pyцепочка наследования FileNotFoundError
cls = FileNotFoundError
chain = []

while cls is not object:
    chain.append(cls.__name__)
    cls = cls.__bases__[0]

print(" ← ".join(chain))
Выводпуть до корня
FileNotFoundError ← OSError ← Exception ← BaseException

Проверить конкретную связь ещё проще: issubclass(FileNotFoundError, OSError) вернёт True. Это тот же инструмент, что isinstance() из главы 6.11, но для классов.

§14Выбор широты перехвата

Иерархия даёт свободу: одну и ту же ошибку можно поймать на любом уровне. Выбор зависит от того, что обработчик собирается делать.

ЗаписьЛовитКогда уместно
except FileNotFoundErrorтолько отсутствие файлаесть конкретное решение: создать файл
except OSErrorлюбую системную проблемуреакция одна на все сбои ввода-вывода
except Exceptionвсе ошибки программыверхний уровень: залогировать и сообщить
except BaseExceptionещё и Ctrl+Cпрактически никогда

Принцип: ловить настолько узко, насколько позволяет обработчик. Если реакция — «попробовать другой путь к файлу», нужен конкретный тип. Если «записать в журнал и продолжить» — годится Exception.

§15Частые ошибки

Что написаноЧто произойдётКак правильно
except BaseExceptionперехватится и Ctrl+C, и выход из программыexcept Exception
голый except:то же самое: это BaseExceptionуказать тип явно
except Exception перед except OSErrorвторой блок недостижимот частного к общему
перечислять IndexError и KeyErrorлишний кодexcept LookupError
except Exception ради одной ожидаемой ошибкискроются и посторонние сбоиуказать конкретный тип
ловить StopIteration вручную в циклецикл for делает это самне перехватывать
считать EnvironmentError отдельным типомэто просто синоним OSErrorиспользовать OSError

§16Проверьте себя

Почему перехват LookupError заменяет два блока?

IndexError и KeyError наследуют от него, а перехват родителя ловит всех потомков.

Чем Exception отличается от BaseException?

Exception — все обычные ошибки программы. BaseException дополнительно включает SystemExit и KeyboardInterrupt, которые управляют ходом выполнения и перехватывать которые не нужно.

Что случится, если написать except BaseException в длинном цикле?

Программа перестанет реагировать на Ctrl+C: нажатие превратится в перехваченное исключение, и цикл продолжится.

Почему UnicodeError наследует от ValueError?

Байты имеют верный тип, но недопустимы для выбранной кодировки — это ровно тот случай, который описывает ValueError.

Как узнать, от кого наследует незнакомое исключение?

Пройти по __bases__ до object или проверить связь через issubclass().

По какому принципу выбирать широту перехвата?

Настолько узко, насколько позволяет обработчик: тип должен соответствовать тому, что этот обработчик действительно умеет чинить.

§17Шпаргалка

Общий перехватexcept Exception as e:
Контейнерыexcept LookupError:
Файлы и сетьexcept OSError as e:
Математикаexcept ArithmeticError:
Проверить связьissubclass(A, B)
Тип пойманногоtype(e).__name__
Не ловитьBaseExceptionexcept:

Итог главы

Исключения образуют дерево наследования, и перехват родителя ловит всех потомков — поэтому вместо десятков имён достаточно помнить несколько узлов: ArithmeticError, LookupError, OSError, ValueError. Корень BaseException включает SystemExit и KeyboardInterrupt, которые ошибками не являются, — поэтому общий перехват пишут как except Exception, а голый except: не используют вовсе. Широту перехвата выбирают по тому, что обработчик реально способен сделать.

Глава 8.3 · Иерархия исключений · Справочник по Python Автор: Макаров Максим Николаевич