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

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

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

Повторное возбуждение: голый 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()
Если проброшенное исключение никто не поймает, программа упадёт с полноценным traceback: сначала напечатается сообщение из print, затем путь от строки вызова до исходной int('abc') внутри load_setting, и в конце — ValueError: invalid literal for int() with base 10: 'abc'. Место, где всё началось, не потеряно: стек сохраняется.

raise и raise e: две формы проброса

Внутри except можно написать и raise e — если исключение поймано с as e, объект и так лежит в переменной. Работает: то же исключение долетит наверх. Но голый raise предпочтительнее: он короче, не сломается при переименовании переменной и не добавляет в traceback лишнюю запись о строке повторного возбуждения. Трассировка после голого raise читается чище — только реальный путь ошибки.

raise e тоже доставляет наверх
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'
голый raise вне except — ошибка сам по себе
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__)
Проверь себя
0 / 6

1. Что делает голый raise внутри блока except?

2. Чем опасен except, который только печатает сообщение и не пробрасывает?

3. Что произойдёт при попытке написать голый raise вне блока except?

4. В чём суть паттерна log and re-raise?

5. Что сохраняется при повторном возбуждении голым raise?

6. Почему голый raise предпочитают записи raise e?

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

Напиши функцию to_int(text): внутри она пробует вернуть int(text), а при ValueError печатает «не разобралось: {текст}» и пробрасывает исключение голым raise. Снаружи вызови to_int('семь') в try, поймай исключение и напечатай «наверху: {тип ошибки}».

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

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

TelegramVK

Что дальше

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

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

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