contextlib.contextmanager и поведение при исключениях

contextlib.contextmanager и поведение при исключениях

@python_quiz

Разберем этот квиз

Короткое название: Поведение contextmanager при исключениях

Код квиза

Исходный код, который разбираем:

python

Задача — понять, что напечатает программа и какие строки выполнятся при выброшенном внутри with исключении.

Ключевая идея

contextlib.contextmanager превращает генераторную функцию в объект контекстного менеджера. Поведение простое:

  • Код перед yield выполняется при входе в with (аналог __enter__).
  • Код после yield выполняется при выходе из with (аналог __exit__), но если внутри блока with произошло исключение, то это исключение «вбрасывается» обратно в генератор в точке yield.
  • Если генератор не обрабатывает (не перехватывает) это исключение, то код, стоящий после yield, не будет выполнен — исключение пройдёт дальше.

Итого: чтобы код после yield гарантированно выполнился, нужно либо обработать исключение внутри генератора, либо использовать конструкцию try...finally вокруг yield.

Подробный пошаговый разбор выполняемого кода

  1. Входим в контекст: вызывается генератор, выполняется print('enter') — на экран выводится enter.
  2. Выполняется yield ['x'] — вызывающий код в with получает ссылку на список ['x'].
  3. Внутри with выполняются инструкции: data.append('y') изменяет список на ['x', 'y'].
  4. Затем внутри with выполняется raise ValueError — исключение возникает.
  5. При попытке выйти из with декоратор contextmanager пытается передать исключение в генератор (используется gen.throw(...)). Внутри генератора исключение выбрасывается в точке yield.
  6. Генератор не перехватывает исключение и поэтому не продолжает выполнение и не достигает print('exit'). Исключение выходит наружу.
  7. Код после with (print(data)) не выполнится, пока исключение не будет кем-то перехвачено во внешнем коде.

Таким образом фактический вывод: будет напечатано только enter, затем произойдёт ValueError (и, если его не перехватить, появится traceback).

Анализ предложенных вариантов ответов

Вот как соотносятся текстовые варианты с реальным поведением:

  • "Напечатает 'enter' и 'exit', затем поднимется исключение; print(data) не выполнится" — не соответствует: exit не будет напечатан без обработки исключения внутри генератора.
  • "Выведет ['x','y'] и напечатает 'enter' и 'exit', затем исключение" — не соответствует: вывод списка не происходит (print(data) не выполнится), и exit не напечатается.
  • "Выведет только 'enter', затем исключение, 'exit' не выполнится" — соответствует описанному поведению.
  • "Ничего не выведет, сразу будет исключение" — не соответствует: enter точно напечатается перед yield.

Как изменить код, чтобы код после yield выполнялся при исключении

Если вы хотите, чтобы при любом выходе из контекста (включая исключения) всегда выполнялся блок очистки, используйте try...finally внутри генератора:

python

Вывод этой версии будет:

  • enter
  • exit
  • caught ValueError
  • after with, data = ['x', 'y']

Заметьте: finally гарантирует выполнение print('exit'), но сам генератор не подавляет исключение — он лишь выполняет очистку; исключение по умолчанию продолжит распространяться, если его не перехватить снаружи.

Вывод

При использовании contextlib.contextmanager код после yield выполняется как часть выхода из контекста, но если внутри with произошло исключение и генератор не обрабатывает его, то этот код не выполнится. Чтобы гарантировать выполнение очистки, оборачивайте yield в try...finally или явно перехватывайте и обрабатывайте исключения внутри генератора.

Report Page