Перейти к содержанию

432 уроков, 26 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 9 из 20 Средний 15 мин 100 XP

Иерархия исключений 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 — большой ствол, в который входит почти всё, что ловят в обычном коде.

проверяем родство через issubclass
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, при всём сходстве, друг другу роднёй не приходятся.

РодительДетиЗа что отвечает
ArithmeticErrorZeroDivisionError, OverflowErrorматематика: деление на ноль, переполнение числа
LookupErrorIndexError, KeyErrorпоиск: нет индекса в списке, нет ключа в словаре
OSErrorFileNotFoundError, PermissionErrorсистема: файл не найден, доступ запрещён
ExceptionValueError, 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. Первый прилетает, когда пользователь прерывает программу сочетанием клавиш, второй — когда программа сама просит себя завершить. Это не ошибки данных, а сигналы управления жизнью программы, и их ловить обычным кодом не положено. Проверим родство:

сигналы прерывания — не Exception
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))
Проверь себя
0 / 6

1. Кто прямой родитель ZeroDivisionError?

2. Что вернёт issubclass(KeyError, LookupError)?

3. Сработает ли except LookupError при возникновении IndexError?

4. В каком порядке ставить ветки except KeyError и except LookupError?

5. Какой тип не будет пойман веткой except Exception?

6. Что напечатает type(e).__name__ в ветке except ArithmeticError, если случилось деление на ноль?

Карточки терминов
Запомнено: 0 / 8
Практика

Проверь родство и поймай ошибку через родителя. Напечатай результат issubclass(KeyError, LookupError). Затем возьми значение data['нет'] из пустого словаря data = {} в блоке try и поймай исключение через except LookupError, напечатав «поймали: {тип ошибки}».

practice.py
Вопросы и ответы по уроку

Как узнать родителя исключения в 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.