Справочник по Python · модуль 8 · исключения и файлы
Глава 8.3
Иерархия
исключений
Встроенных исключений больше шестидесяти, а со стандартной библиотекой — несколько сотен. Запоминать их по одному бессмысленно: они объединены в дерево наследования, и перехват родителя ловит всех потомков. Знание нескольких узлов этого дерева заменяет знание сотен имён.
- Категория
- Обработка ошибок
- Уровень
- Средний
- Связанные темы
- Наследование, типы ошибок, отладка
- Зачем это знать
- Выбирать правильную широту перехвата
§1Проблема количества
Обработка исключений кажется хаотичной, и на то есть причины. Только встроенных исключений больше шестидесяти. Заранее определить, какие из них возможны в конкретном месте, обычно нельзя: типы исключений не указываются в сигнатуре функции, и никакой инструмент не проверит, что вы обработали всё нужное.
Выход не в том, чтобы выучить список, а в том, чтобы понять его устройство. Исключения — это классы, связанные наследованием, и перехват родителя ловит всех его потомков. Значит, вместо десятка конкретных типов часто достаточно одной категории.
§2Родитель ловит потомков
Задача: достать элемент из контейнера, который может оказаться и списком, и словарём. Ошибки будут разные — IndexError или KeyError.
try: item = items[index] except IndexError: # выдаётся, если items — последовательность ... except KeyError: # выдаётся, если items — отображение ...
Оба типа наследуют от общего родителя, поэтому запись сокращается вдвое.
try: item = items[index] except LookupError: ...
LookupError — категория «ошибка выборки из контейнера». Она достаточно широка, чтобы покрыть оба случая, и достаточно узка, чтобы не поймать что-то постороннее.
§3Пример: LookupError
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Общее дерево
- корень и обычная граница перехвата
- категории и конкретные ошибки
- служебные: перехватывать не следует
Восемь узлов сверху — это почти всё, что нужно помнить. Остальные сотни исключений висят на них листьями.
§5BaseException и Exception
Корень дерева — BaseException. От него наследует всё, но напрямую — только три особых исключения, которые управляют ходом программы, а не сообщают об ошибке.
| Класс | Что означает | Ловить? |
|---|---|---|
BaseException | корень всей иерархии | практически никогда |
SystemExit | намеренное завершение программы | нет |
KeyboardInterrupt | пользователь нажал Ctrl+C | нет |
Exception | все обычные ошибки | да, если нужен общий перехват |
Отсюда практическое правило: ловят Exception, а не BaseException. Все ошибки, относящиеся к работе программы, наследуют от Exception.
§6Почему не except BaseException
Разница на первый взгляд формальная, но последствия ощутимые.
print(issubclass(KeyboardInterrupt, Exception)) print(issubclass(KeyboardInterrupt, BaseException))
False True
Если написать except BaseException в длинном цикле, программа перестанет реагировать на Ctrl+C: нажатие превратится в перехваченное исключение, и цикл продолжит работу. То же с SystemExit — программа не сможет завершиться. Именно поэтому голый except: из главы 8.1 плох: он равнозначен except BaseException.
§7ArithmeticError
Категория математических ошибок: ZeroDivisionError, OverflowError, FloatingPointError.
try: 1 / 0 except ArithmeticError as e: print(type(e).__name__)
ZeroDivisionError
§8LookupError
Категория ошибок выборки из контейнера — та самая из §2. Потомков всего два, зато встречаются они постоянно.
| Исключение | Когда |
|---|---|
IndexError | индекс за границами списка, кортежа, строки |
KeyError | ключа нет в словаре или множестве |
Категория намеренно узкая: она не включает ошибки, не связанные с выборкой значений. Это и делает её удобной — перехват не поймает лишнего.
§9OSError
Самая широкая из категорий: всё, что приходит от операционной системы, — файлы, сетевые подключения, права доступа, каналы, тайм-ауты.
try: open("нет-такого.txt") except OSError as e: print(type(e).__name__) print(e.errno, e.filename)
FileNotFoundError 2 нет-такого.txt
Это то самое исключение, о котором говорилось в главе 8.2: его args содержат не только текст. Атрибуты errno и filename позволяют разобраться, что именно не получилось. Работа с файлами начинается со следующей главы, и OSError там будет постоянным спутником. Старое имя EnvironmentError — синоним OSError.
§10ValueError и UnicodeError
ValueError означает «тип аргумента верный, но само значение недопустимо» — с ним встречались, начиная с главы 1.5.
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.
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: не используют вовсе. Широту перехвата выбирают по тому, что обработчик реально способен сделать.