contextlib.contextmanager и поведение при исключениях
@python_quizРазберем этот квиз
Короткое название: Поведение contextmanager при исключениях
Код квиза
Исходный код, который разбираем:

Задача — понять, что напечатает программа и какие строки выполнятся при выброшенном внутри with исключении.
Ключевая идея
contextlib.contextmanager превращает генераторную функцию в объект контекстного менеджера. Поведение простое:
- Код перед
yieldвыполняется при входе в with (аналог__enter__). - Код после
yieldвыполняется при выходе из with (аналог__exit__), но если внутри блока with произошло исключение, то это исключение «вбрасывается» обратно в генератор в точкеyield. - Если генератор не обрабатывает (не перехватывает) это исключение, то код, стоящий после
yield, не будет выполнен — исключение пройдёт дальше.
Итого: чтобы код после yield гарантированно выполнился, нужно либо обработать исключение внутри генератора, либо использовать конструкцию try...finally вокруг yield.
Подробный пошаговый разбор выполняемого кода
- Входим в контекст: вызывается генератор, выполняется
print('enter')— на экран выводится enter. - Выполняется
yield ['x']— вызывающий код в with получает ссылку на список['x']. - Внутри with выполняются инструкции:
data.append('y')изменяет список на['x', 'y']. - Затем внутри with выполняется
raise ValueError— исключение возникает. - При попытке выйти из with декоратор contextmanager пытается передать исключение в генератор (используется gen.throw(...)). Внутри генератора исключение выбрасывается в точке yield.
- Генератор не перехватывает исключение и поэтому не продолжает выполнение и не достигает
print('exit'). Исключение выходит наружу. - Код после 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 внутри генератора:

Вывод этой версии будет:
- enter
- exit
- caught
ValueError - after with, data = ['x', 'y']
Заметьте: finally гарантирует выполнение print('exit'), но сам генератор не подавляет исключение — он лишь выполняет очистку; исключение по умолчанию продолжит распространяться, если его не перехватить снаружи.
Вывод
При использовании contextlib.contextmanager код после yield выполняется как часть выхода из контекста, но если внутри with произошло исключение и генератор не обрабатывает его, то этот код не выполнится. Чтобы гарантировать выполнение очистки, оборачивайте yield в try...finally или явно перехватывайте и обрабатывайте исключения внутри генератора.