Иерархия исключений Python: кто чей родитель
ZeroDivisionError, KeyError, FileNotFoundError — не набор случайных имён, а семейное дерево. Разбираемся, кто чей родитель и почему ловля родителя ловит детей.
Редакция Питоники
К этому моменту курса вы встречались с ValueError, ZeroDivisionError, KeyError — и могло показаться, что это просто набор имён без внутренней логики. На самом деле исключения Python образуют семейное дерево: у каждого класса есть родители, у родителей — свои дети. Знание родства объясняет удивительное поведение цепочки except: один обработчик ловит сразу несколько типов ошибок, а порядок веток решает, кто сработает.
Родословная класса: __mro__
Каждый класс исключения знает свою родословную. Её показывает атрибут __mro__ — цепочка классов от самого класса до корня всех объектов Python. Посмотрим на родословную уже знакомого ZeroDivisionError:
for cls in ZeroDivisionError.__mro__:
print(cls.__name__)
ZeroDivisionError ArithmeticError Exception BaseException object
Читается цепочка снизу вверх: ZeroDivisionError — сын ArithmeticError, тот — сын Exception, а над всеми стоит BaseException, корень системы исключений, и наконец object, общий предок всего в Python. Два новых имени появились не случайно: ArithmeticError — семейная ветка математики (деление на ноль, переполнение числа), а Exception — большой ствол, в который входит почти всё, что ловят в обычном коде.
pairs = [
(ZeroDivisionError, ArithmeticError),
(KeyError, LookupError),
(IndexError, LookupError),
(ValueError, Exception),
(ValueError, TypeError),
]
for child, parent in pairs:
print(child.__name__, '->', parent.__name__, ':', issubclass(child, parent))
ZeroDivisionError -> ArithmeticError : True KeyError -> LookupError : True IndexError -> LookupError : True ValueError -> Exception : True ValueError -> TypeError : False
Функция issubclass отвечает на вопрос «чьё это дитя» без всяких догадок. Из ответов видно и неожиданное родство: KeyError и IndexError — братья, у них общий родитель LookupError, ветка поиска. Один из них знаком вам по словарям, другой по спискам, а оба случаются, когда искомое не нашлось. А ValueError и TypeError, при всём сходстве, друг другу роднёй не приходятся.
| Родитель | Дети | За что отвечает |
|---|---|---|
| ArithmeticError | ZeroDivisionError, OverflowError | математика: деление на ноль, переполнение числа |
| LookupError | IndexError, KeyError | поиск: нет индекса в списке, нет ключа в словаре |
| OSError | FileNotFoundError, PermissionError | система: файл не найден, доступ запрещён |
| Exception | ValueError, TypeError и почти все остальные | общий предок обычных ошибок |
Ловля родителя ловит детей
Практический смысл иерархии — в работе цепочки except. Проверка «это ArithmeticError?» срабатывает и для ZeroDivisionError, потому что он в этой семье:
try:
1 / 0
except ArithmeticError as e:
print('поймали', type(e).__name__, 'через родителя ArithmeticError')
поймали ZeroDivisionError через родителя ArithmeticError
Ловля родителя ловит и всех его детей — один обработчик вместо длинного списка типов. Обратите внимание: напечатанное имя — ZeroDivisionError, настоящее имя пойманного исключения, а не имя родителя из заголовка. Проверка в except — это вопрос «подходит ли к этой семье», а сам объект ошибки остаётся собой. Отсюда и правило порядка веток в цепочке except из четвёртого урока: проверка идёт сверху вниз, и побеждает первая подходящая ветка.
data = {}
try:
data['город']
except KeyError:
print('ветка ребёнка: поймали KeyError')
except LookupError:
print('ветка родителя: сюда не дошли')
ветка ребёнка: поймали KeyError
data = {}
try:
data['город']
except LookupError:
print('родитель стоит первым — поймал он')
except KeyError:
print('до ветки ребёнка очередь не дошла')
родитель стоит первым — поймал он
Две одинаковые программы с переставленными ветками дают разный результат. KeyError в паре с LookupError — тот случай, когда порядок реально влияет на жизнь: родитель перехватил ошибку раньше, и специфичная ветка ребёнка стала мёртвым кодом. Python при этом не ругается и не предупреждает — он честно идёт по цепочке сверху вниз и берёт первое совпадение.
BaseException: самый верх дерева
На вершине дерева стоят Exception и его собрат BaseException. Разница между ними принципиальна: у BaseException есть ещё двое детей — KeyboardInterrupt и SystemExit. Первый прилетает, когда пользователь прерывает программу сочетанием клавиш, второй — когда программа сама просит себя завершить. Это не ошибки данных, а сигналы управления жизнью программы, и их ловить обычным кодом не положено. Проверим родство:
print('KeyboardInterrupt - потомок Exception:', issubclass(KeyboardInterrupt, Exception))
print('KeyboardInterrupt - потомок BaseException:', issubclass(KeyboardInterrupt, BaseException))
print('SystemExit - потомок Exception:', issubclass(SystemExit, Exception))
KeyboardInterrupt - потомок Exception: False KeyboardInterrupt - потомок BaseException: True SystemExit - потомок Exception: False
Вот почему в обычном коде ловят Exception, а не BaseException: ветка Exception пропускает сигналы прерывания мимо себя, и пользователь всегда может остановить зависший скрипт. Ловля BaseException отняла бы и этот рычаг. Подробнее о границе между «ловить конкретное» и «ловить почти всё» — в пятнадцатом уроке.
Иерархия работает в обе стороны: не только встроенные исключения состоят в родстве, но и ваши собственные классы могут занять место в дереве — с общим предком для целого семейства своих ошибок. Этим займётся десятый урок о своих исключениях.
Сначала предскажи ответ в голове — это главный навык программиста.
for cls in ZeroDivisionError.__mro__:
print(cls.__name__)
try:
[1, 2, 3][10]
except LookupError:
print('поймали через родителя')
for cls in [ValueError, KeyError]:
print(cls.__name__, issubclass(cls, LookupError))
1. Кто прямой родитель ZeroDivisionError?
2. Что вернёт issubclass(KeyError, LookupError)?
3. Сработает ли except LookupError при возникновении IndexError?
4. В каком порядке ставить ветки except KeyError и except LookupError?
5. Какой тип не будет пойман веткой except Exception?
6. Что напечатает type(e).__name__ в ветке except ArithmeticError, если случилось деление на ноль?
Проверь родство и поймай ошибку через родителя. Напечатай результат issubclass(KeyError, LookupError). Затем возьми значение data['нет'] из пустого словаря data = {} в блоке try и поймай исключение через except LookupError, напечатав «поймали: {тип ошибки}».
Как узнать родителя исключения в Python?
Напечатать его родословную: for cls in ZeroDivisionError.__mro__: print(cls.__name__) покажет цепочку от класса до object. Точечная проверка родства делается функцией issubclass(ZeroDivisionError, ArithmeticError) — она отвечает True или False.
Урок 4: несколько except — разные реакцииПоймает ли except ArithmeticError ZeroDivisionError?
Да: ловля родителя ловит всех его детей. ZeroDivisionError — дитя ArithmeticError, поэтому один обработчик по родителю перехватывает всё математическое семейство, включая OverflowError. Объект ошибки при этом сохраняет настоящее имя — его печатает type(e).__name__.
Какие исключения у KeyError и IndexError общие?
Родитель LookupError — ветка поиска. KeyError возникает, когда ключа нет в словаре, IndexError — когда индекса нет в списке, но для Python это одна и та же ситуация «искомое не нашлось». Поэтому except LookupError ловит обе ошибки.
Курс Словари: доступ по ключу и KeyErrorЧем BaseException отличается от Exception?
BaseException — корень всего дерева, а Exception — его главный ствол для обычных ошибок. У BaseException есть ещё дети KeyboardInterrupt и SystemExit: сигналы прерывания и завершения. В обычном коде ловят Exception — тогда пользователь всегда может остановить программу, а сигналы не будут проглочены.
Урок 15: except Exception — когда ловить всёПонравился урок? Сошлитесь на него
«Ловля родителя ловит и всех его детей — один обработчик вместо длинного списка типов.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Исключения · Урок 4
Несколько except: разные ошибки — разные реакции
Цепочка except-блоков: ValueError — один план, ZeroDivisionError — другой. Порядок обработчиков, правило одного обработчика и кортеж типов.
Исключения · Урок 10
Свои исключения в Python: класс от Exception
ValueError говорит «что-то не то», но не «какое правило нарушено». Заводим свои классы исключений: внятное имя в логах, свои атрибуты и целые семейства ошибок.
Исключения · Урок 15
except Exception в Python: когда ловить всё и чем это грозит
Одна строка вместо перечня типов: except Exception ловит почти всё. Разбираем слепую зону перехвата, спрятанные баги и место, где широкая сетка уместна по-настоящему.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
pytest · Урок 9
Проверка исключений: pytest.raises
Хорошая функция на плохом входе не молчит, а бросает исключение — и тест обязан это проверять. pytest.raises делает это одной строкой внутри with.
pytest.raises проверка исключенийpytest проверка исключения
asyncio · Урок 11
Исключения в конкурентном коде
Одна корутина с ValueError способна уронить весь gather. Флаг return_exceptions=True превращает исключения в значения списка — пачка выживает, а отчёт собирается целиком.
asyncio исключения gatherTask exception was never retrieved
Исключения · Урок 1
Что такое исключение: первая ошибка в программе
Первая ошибка в программе: читаем traceback за десять секунд — тип ошибки, место и сообщение. Разбираем четыре типовых падения на живом коде.
python исключенияисключения python