Справочник по Python · модуль 8 · исключения и файлы
Глава 8.2
raise и цепочки
исключений
Исключения не только ловят — их выбрасывают сами. raise сигнализирует об ошибке из глубины кода, необработанное исключение поднимается вверх по стеку вызовов, а конструкция from связывает новую ошибку с исходной, сохраняя причину.
- Категория
- Обработка ошибок
- Уровень
- Средний
- Связанные темы
- Функции, стек вызовов, отладка
- Зачем это знать
- Сообщать об ошибке там, где она обнаружена, не теряя причину
§1Зачем выбрасывать ошибку
Функция обнаружила недопустимые данные. Что ей делать? Вернуть None — вызывающий код может не проверить и продолжит работать с пустотой. Напечатать сообщение — функция не знает, есть ли вообще экран у программы.
return None вызывающий может не заметить
print("ошибка") сообщение уйдёт в пустоту, работа продолжится
raise ValueError(...) выполнение прервано, причина названа
Оператор raise решает задачу: место, где ошибка обнаружена, отделяется от места, где принимается решение, что с ней делать. Функция сообщает о проблеме, а вызывающий код решает — перехватить или пропустить дальше.
§2Синтаксис raise
raise ТипИсключения("сообщение об ошибке")
└────┬─────┘ └────────┬────────┘
ValueError, необязательный
TypeError и т. д. текст причины
Тип берут из встроенных — ValueError, TypeError, ZeroDivisionError и другие из главы 8.3. Сообщение необязательно, но без него отладка становится гаданием.
§3Проверка аргументов
Самое частое применение — проверить входные данные в начале функции.
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 уместен везде, где логика программы обнаружила недопустимое состояние — не только при делении.
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 — перехвачено здесь ↑ верхний уровень ← сюда уже не дойдёт
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 без аргументов.
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
Исключение — обычный объект, и у него есть атрибуты. Первый из них — кортеж аргументов, переданных при создании.
try: raise ValueError("плохое значение", 42) except ValueError as e: print(e.args) print(e.args[1])
('плохое значение', 42)
42Обычно в args лежит одна строка с описанием. Но некоторые исключения — например, OSError из главы 8.4 — передают в нём и код ошибки, и имя файла.
§9from: явная причина
Частая ситуация: низкоуровневая ошибка не должна «протекать» наружу, и вместо неё выбрасывают более понятную. Но и терять исходную причину нельзя.
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__.
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
Иногда исходная ошибка — деталь реализации, и показывать её наружу не нужно. Тогда цепочку подавляют явно.
try: try: int("abc") except ValueError: raise RuntimeError("не удалось разобрать настройки") from None except RuntimeError as e: print(e) print("cause:", e.__cause__)
не удалось разобрать настройки cause: None
Пользоваться этим стоит осторожно: подавленная причина усложняет отладку. Оправдано, когда внутренняя ошибка ничего не говорит тому, кто читает сообщение.
§13Атрибут __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 вне блока except | RuntimeError: 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(...)raiseraise RuntimeError(...) from eraise ... from Nonee.argstraceback.print_exception(e)Итог главы
raise позволяет функции сообщить об ошибке там, где та обнаружена, не решая, что с ней делать. Необработанное исключение поднимается по стеку вызовов, и это даёт свободу ставить обработчик на том уровне, где есть контекст для решения. Голый raise внутри except пробрасывает ошибку дальше с сохранением трейсбека, а from связывает новое исключение с исходным через __cause__ — в отличие от __context__, который заполняется автоматически.