Повторное возбуждение: голый raise в except
Исключение поймано, но проблема не решена: голый raise внутри except передаёт её наверх, не теряя стек. Учимся честному пробросу и ловим RuntimeError от raise вне except.
Редакция Питоники
Пойманное исключение — ещё не решённая проблема. Иногда внутри except положение можно исправить: подставить значение по умолчанию, спросить данные заново, пропустить бракованную строку. Но чаще ловящий код знает не больше того, кто вызвал его с плохими данными, — решение должен принимать вызывающий. Для такого случая в Python есть приём: поймать исключение, сделать что можно и пробросить его дальше голым raise.
Проглотили и забыли: except без проброса
Начнём с ошибки, которую легко совершить: поймали исключение, напечатали сообщение — и на этом успокоились. Программа не падает, выглядит рабочей, но вызывающий код даже не подозревает, что операция провалилась:
def load_setting():
try:
int('не число')
except ValueError:
print('внутри: что-то не так, но я промолчу')
print('вызов начался')
load_setting()
print('вызов закончился: вызывающий ни о чём не догадался')
вызов начался внутри: что-то не так, но я промолчу вызов закончился: вызывающий ни о чём не догадался
Функция должна была вернуть настройку, а вернула пустоту — и никто об этом не узнал. Такое называют глотанием исключений: ошибка поймана и спрятана, программа продолжает работать с плохими данными. Баг потом ищут в другом месте: падает не там, где сломалось, а там, где накопившаяся пустота наконец взорвалась. Правило честное: если вы не можете решить проблему внутри except — дайте об этом знать наверх.
Голый raise: поймал — передай дальше
Для передачи наверх у raise есть особая форма — без аргументов вовсе. Внутри блока except голый raise означает: возьми текущее исключение и продолжи его путь, как будто обработчика и не было.
def load_setting():
try:
int('не число')
except ValueError:
print('журнал: настройка не разобралась, пробрасываю дальше')
raise
try:
load_setting()
except ValueError as e:
print('наверху поймали:', type(e).__name__, '-', str(e))
журнал: настройка не разобралась, пробрасываю дальше наверху поймали: ValueError - invalid literal for int() with base 10: 'не число'
Поймали, сделали что могли — записали в журнал, — и передали дальше. Это самый частый паттерн повторного возбуждения, у него даже есть имя: log and re-raise. Голый raise внутри except не создаёт новое исключение, а продолжает путь текущего: тот же объект, тот же тип, то же сообщение, которое читается через as e из пятого урока. Вызывающий код получает ровно ту ошибку, что возникла, со всеми подробностями.
def parse_record(line):
try:
return int(line)
except ValueError:
print('журнал: строка не разобралась')
raise
for line in ['10', 'двадцать']:
try:
print('значение:', parse_record(line))
except ValueError:
print('пропускаем строку и идём дальше')
значение: 10 журнал: строка не разобралась пропускаем строку и идём дальше
Исключение путешествует и через границы функций: внутренний обработчик пробросил — внешний, в совсем другой части программы, поймал и принял решение. Разделение труда получилось честным: нижний уровень знает, как записать в журнал, верхний — как пережить сбой. Заметьте: строка «значение: 10» успела напечататься до того, как вторая строка сломалась. Проброс отменяет только текущий шаг, а не всю работу.
def load_setting():
try:
int('abc')
except ValueError:
print('не смог прочитать конфиг')
raise
load_setting()
raise и raise e: две формы проброса
Внутри except можно написать и raise e — если исключение поймано с as e, объект и так лежит в переменной. Работает: то же исключение долетит наверх. Но голый raise предпочтительнее: он короче, не сломается при переименовании переменной и не добавляет в traceback лишнюю запись о строке повторного возбуждения. Трассировка после голого raise читается чище — только реальный путь ошибки.
def load_setting():
try:
int('abc')
except ValueError as e:
raise e
try:
load_setting()
except ValueError as e:
print('поймали то же исключение:', type(e).__name__, '-', str(e))
поймали то же исключение: ValueError - invalid literal for int() with base 10: 'abc'
try:
raise
except RuntimeError as e:
print('поймали:', type(e).__name__, '-', str(e))
поймали: RuntimeError - No active exception to reraise
Голому raise нужно активное исключение: он по определению повторяет то, что летит прямо сейчас. Вне except такого нет — и Python поднимает RuntimeError, уже свою собственную ошибку кода, а не данных.
Повторное возбуждение — честный способ передать ошибку наверх, ничего не скрывая. А когда пробросу предшествует новое исключение, обёртывающее старое как причину, в дело вступает конструкция raise ... from ... — ей посвящён семнадцатый урок о цепочках причин.
Сначала предскажи ответ в голове — это главный навык программиста.
def f():
try:
int('x')
except ValueError:
print('внутри')
raise
try:
f()
except ValueError:
print('снаружи')
try:
try:
1 / 0
except ZeroDivisionError:
print('внутренний: пробрасываю')
raise
except ArithmeticError as e:
print('внешний:', type(e).__name__)
try:
raise
except RuntimeError as e:
print(type(e).__name__)
1. Что делает голый raise внутри блока except?
2. Чем опасен except, который только печатает сообщение и не пробрасывает?
3. Что произойдёт при попытке написать голый raise вне блока except?
4. В чём суть паттерна log and re-raise?
5. Что сохраняется при повторном возбуждении голым raise?
6. Почему голый raise предпочитают записи raise e?
Напиши функцию to_int(text): внутри она пробует вернуть int(text), а при ValueError печатает «не разобралось: {текст}» и пробрасывает исключение голым raise. Снаружи вызови to_int('семь') в try, поймай исключение и напечатай «наверху: {тип ошибки}».
Как пробросить исключение дальше после обработки в Python?
Внутри блока except написать raise без аргументов: он повторно возбудит текущее исключение с исходным traceback. Частый паттерн — log and re-raise: сначала печать или запись в журнал, затем голый raise, и ошибка летит наверх без потерь.
Урок 7: raise — бросаем исключение самиЧем голый raise отличается от raise e?
Обе записи пробрасывают то же исключение, но голый raise короче, не зависит от имени переменной из as e и не добавляет в traceback лишнюю запись о строке повторного возбуждения. raise e остаётся рабочим вариантом — просто менее аккуратным.
Что будет, если написать raise вне except?
Голому raise нужно активное исключение, а вне обработчика его нет — Python поднимет RuntimeError с сообщением No active exception to reraise. Это ошибка кода, а не данных: голый проброс возможен только внутри except или finally.
Теряется ли traceback при повторном возбуждении?
Нет: голый raise продолжает путь существующего исключения вместе с его стеком. В итоговой трассировке видно и исходное место ошибки, и все уровни, через которые она прошла, — обрезать или переписывать путь не нужно.
Урок 17: raise from — цепочка причинПонравился урок? Сошлитесь на него
«Но чаще ловящий код знает не больше того, кто вызвал его с плохими данными, — решение должен принимать вызывающий.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Исключения · Урок 7
raise: бросаем исключение сами
До сих пор ошибки бросал Python. Теперь научимся бросать сами: raise ValueError с понятным текстом, когда данные нарушают правила программы.
Исключения · Урок 5
as e: объект ошибки в переменной
Обработчик узнаёт больше, если взять ошибку в переменную: except ValueError as e даёт тип, сообщение и аргументы исключения.
Исключения · Урок 17
raise from в Python: цепочка причин, __cause__ и __context__
Обработчик превращает чужую ошибку в свою — а куда девается первоначальная? Python хранит её в цепочке: raise from e помечает причину явно, from None прячет её сознательно.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
pytest · Урок 9
Проверка исключений: pytest.raises
Хорошая функция на плохом входе не молчит, а бросает исключение — и тест обязан это проверять. pytest.raises делает это одной строкой внутри with.
pytest проверка исключенияpytest DID NOT RAISE
aiogram · Урок 9
Обработка ошибок в телеграм-боте: try/except, лимиты и логи
Учим бот переживать ошибки: try/except в хендлерах, лимиты Telegram с RetryAfter, модуль logging вместо print и перезапуск polling после падения.
try except pythonголый except python
Исключения · Урок 10
Свои исключения в Python: класс от Exception
ValueError говорит «что-то не то», но не «какое правило нарушено». Заводим свои классы исключений: внятное имя в логах, свои атрибуты и целые семейства ошибок.
python свои исключенияpython создать своё исключение