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

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

Глава 8.2

8.2

raise и цепочки
исключений

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

Категория
Обработка ошибок
Уровень
Средний
Связанные темы
Функции, стек вызовов, отладка
Зачем это знать
Сообщать об ошибке там, где она обнаружена, не теряя причину

§1Зачем выбрасывать ошибку

Функция обнаружила недопустимые данные. Что ей делать? Вернуть None — вызывающий код может не проверить и продолжит работать с пустотой. Напечатать сообщение — функция не знает, есть ли вообще экран у программы.

Сравнениетри реакции на неверный аргумент
return None            вызывающий может не заметить
print("ошибка")        сообщение уйдёт в пустоту, работа продолжится
raise ValueError(...)  выполнение прервано, причина названа

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

§2Синтаксис raise

Схемаобщая форма
raise ТипИсключения("сообщение об ошибке")
      └────┬─────┘   └────────┬────────┘
     ValueError,          необязательный
     TypeError и т. д.    текст причины

Тип берут из встроенных — ValueError, TypeError, ZeroDivisionError и другие из главы 8.3. Сообщение необязательно, но без него отладка становится гаданием.

§3Проверка аргументов

Самое частое применение — проверить входные данные в начале функции.

main.pyфункция сама защищается от нуля
def divide(a, b):
    if b == 0:
        raise ZeroDivisionError("Деление на ноль недопустимо!")
    return a / b

try:
    result = divide(10, 0)
except ZeroDivisionError as e:
    print("Ошибка:", e)
Выводсообщение своё, а не системное
Ошибка: Деление на ноль недопустимо!

Обратите внимание: raise прерывает функцию сразу — строка return a / b не выполнится. Это делает else ненужным, проверки просто пишут в начале.

§4raise в условии

raise уместен везде, где логика программы обнаружила недопустимое состояние — не только при делении.

main.pyпроверка возраста
def set_age(age):
    if age < 0:
        raise ValueError("возраст не может быть отрицательным")
    return age

try:
    set_age(-5)
except ValueError as e:
    print(e)
Выводсвоё правило, свой текст
возраст не может быть отрицательным

Выбор типа не произволен: ValueError означает «тип верный, значение недопустимое» — именно этот случай. Для «передан не тот тип» берут TypeError.

§5Распространение по стеку

Если в функции исключение не перехвачено, оно не исчезает — оно поднимается к тому, кто её вызвал. И так далее, пока не найдётся обработчик.

Схемапуть исключения вверх
divide()      ← здесь возникло, обработчика нет
   ↑
main()        ← есть except ZeroDivisionError — перехвачено здесь
   ↑
верхний уровень   ← сюда уже не дойдёт
main.pyдва уровня обработчиков
def divide(a, b):
    return a / b

def main():
    try:
        divide(10, 0)
    except ZeroDivisionError as e:
        print("Ошибка в main:", e)

try:
    main()
except Exception as e:
    print("Общая ошибка:", e)
Выводвнешний блок не сработал
Ошибка в main: division by zero

Внешний try остался незадействованным: ошибку перехватили раньше. Если бы в main() обработчика не было, исключение поднялось бы выше и вывелось бы «Общая ошибка».

§6Где ставить обработчик

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

УровеньЧто он знаетЧто может сделать
функция вычислениячто данные невернысообщить об этом через raise
функция сценариякакая операция сорваласьпопробовать иначе или отменить шаг
верхний уровеньесть ли пользователь и логпоказать сообщение, записать в журнал

Поэтому низкоуровневые функции обычно не ловят ошибки — они их выбрасывают. Обработчик ставят там, где есть контекст для решения.

§7Повторный выброс

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

main.pyзалогировали и передали дальше
def process(a, b):
    try:
        return a / b
    except ZeroDivisionError as e:
        print("Логируем и пробрасываем:", e)
        raise

try:
    process(10, 0)
except ZeroDivisionError as e:
    print("Поймали снаружи:", e)
Выводошибка обработана дважды
Логируем и пробрасываем: division by zero
Поймали снаружи: division by zero

Голый raise внутри except повторяет то же самое исключение вместе с исходным трейсбеком. Если написать raise e, объект будет тот же, но часть истории вызовов потеряется — поэтому предпочитают короткую форму.

§8Атрибут args

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

main.pyдва аргумента вместо одного
try:
    raise ValueError("плохое значение", 42)
except ValueError as e:
    print(e.args)
    print(e.args[1])
Выводкортеж из главы 4.1
('плохое значение', 42)
42

Обычно в args лежит одна строка с описанием. Но некоторые исключения — например, OSError из главы 8.4 — передают в нём и код ошибки, и имя файла.

§9from: явная причина

Частая ситуация: низкоуровневая ошибка не должна «протекать» наружу, и вместо неё выбрасывают более понятную. Но и терять исходную причину нельзя.

main.pyсвязываем новую ошибку с исходной
try:
    try:
        x = int("abc")
    except ValueError as e:
        raise RuntimeError("Произошла ошибка!") from e
except RuntimeError as e:
    print("Текущее исключение:", e)
    print("Предыдущее исключение:", e.__cause__)
Выводобе ошибки доступны
Текущее исключение: Произошла ошибка!
Предыдущее исключение: invalid literal for int() with base 10: 'abc'

Конструкция from записывает исходное исключение в атрибут __cause__. В трейсбеке это выглядит как строка «The above exception was the direct cause of the following exception» — то есть Python прямо говорит, что одна ошибка вызвала другую.

§10__context__: неявная связь

Если новое исключение возникло во время обработки другого, но связь не указана явно, Python всё равно её запоминает — в атрибуте __context__.

main.pyошибка внутри обработчика ошибки
try:
    try:
        x = 1 / 0
    except ZeroDivisionError:
        int("abc")
except ValueError as e:
    print("Текущее исключение:", e)
    print("Контекст исключения:", e.__context__)
Выводвиден и второй сбой, и первый
Текущее исключение: invalid literal for int() with base 10: 'abc'
Контекст исключения: division by zero

Здесь from никто не писал: связь установилась сама, потому что вторая ошибка случилась внутри блока except. В трейсбеке это даёт формулировку «During handling of the above exception, another exception occurred».

§11cause и context вместе

АтрибутКогда заполняетсяФормулировка в трейсбеке
__cause__только при raise ... from eпрямая причина
__context__автоматически, если ошибка внутри обработчикавозникла при обработке другой

Разница по смыслу: __cause__ означает «я выбросил это потому что», __context__ — «это случилось, пока я разбирался с другим». Первое — намерение разработчика, второе — фиксация обстоятельств. При from заполняются оба атрибута; без него — только __context__.

§12from None

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

main.pyскрываем внутреннюю причину
try:
    try:
        int("abc")
    except ValueError:
        raise RuntimeError("не удалось разобрать настройки") from None
except RuntimeError as e:
    print(e)
    print("cause:", e.__cause__)
Выводтрейсбек покажет только новую ошибку
не удалось разобрать настройки
cause: None

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

§13Атрибут __traceback__

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

main.pyчитаемый вывод через модуль traceback
import traceback

try:
    int("abc")
except ValueError as e:
    traceback.print_exception(e)

Печатать сам e.__traceback__ бессмысленно — это служебный объект. Для читаемого вывода есть модуль traceback из стандартной библиотеки: он выводит ту же картину, что Python печатает при аварийной остановке, но программа при этом продолжает работать.

§14Когда raise уместен

СитуацияРешение
аргумент функции недопустимraise ValueError(...)
аргумент не того типаraise TypeError(...)
нужно залогировать и передать дальшеraise без аргументов
заменить техническую ошибку понятнойraise ... from e
ожидаемая ситуация, а не ошибкавернуть значение, не выбрасывать

Последняя строка важна: исключение — для исключительного. «Пользователь не найден» в поиске — обычный исход, для него достаточно вернуть None. А вот «файл настроек повреждён» — уже причина прервать работу.

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

Что написаноЧто произойдётКак правильно
raise ValueError без сообщенияошибка без пояснения причиныдобавить текст в скобках
raise "текст"TypeError: строка не исключениеraise ValueError("текст")
raise вне блока exceptRuntimeError: No active exception to reraiseуказать тип исключения явно
raise e вместо raiseчасть трейсбека теряетсяголый raise внутри except
raise RuntimeError(...) без from eпричина не видна в сообщениидобавить from e
ловить и выбрасывать заново без причинылишний шум в кодене перехватывать вовсе
raise для обычных исходоввызывающий код обрастает tryвернуть значение

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

Чем raise лучше, чем вернуть None при ошибке?

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

Что происходит, если в функции исключение не перехвачено?

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

Чем голый raise отличается от raise e?

Голый повторяет то же исключение с исходным трейсбеком целиком. При raise e часть истории вызовов теряется.

Чем __cause__ отличается от __context__?

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

Зачем нужен from None?

Чтобы скрыть внутреннюю причину, которая ничего не скажет читателю сообщения. Пользоваться осторожно: отладка усложняется.

Всегда ли ошибку нужно выбрасывать?

Нет. Исключения — для исключительных ситуаций. «Ничего не найдено» в поиске — обычный исход, достаточно вернуть None.

§17Шпаргалка

Выброситьraise ValueError("причина")
Проверка аргументаif x < 0: raise ValueError(...)
Пробросить дальшеraise
Связать с причинойraise RuntimeError(...) from e
Скрыть причинуraise ... from None
Аргументы ошибкиe.args
Читаемый трейсбекtraceback.print_exception(e)

Итог главы

raise позволяет функции сообщить об ошибке там, где та обнаружена, не решая, что с ней делать. Необработанное исключение поднимается по стеку вызовов, и это даёт свободу ставить обработчик на том уровне, где есть контекст для решения. Голый raise внутри except пробрасывает ошибку дальше с сохранением трейсбека, а from связывает новое исключение с исходным через __cause__ — в отличие от __context__, который заполняется автоматически.

Глава 8.2 · raise и цепочки исключений · Справочник по Python Автор: Макаров Максим Николаевич