При таком использовании @contextmanager какое исключение покинет with и почему тело блока не выполнится? пр...

При таком использовании @contextmanager какое исключение покинет with и почему тело блока не выполнится?

from contextlib import contextmanager

@contextmanager
def resource():
    raise OSError("не удалось открыть ресурс")
    yield "ресурс"

with resource() as value:
    print(value)
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Из with выйдет исходный OSError("не удалось открыть ресурс"), а тело блока не выполнится. Исключение возникает до того, как генератор достиг yield, поэтому контекстный менеджер не успевает передать управление телу блока.

Исторический контекст

contextlib.contextmanager появился как способ описывать протокол контекстного менеджера обычной генераторной функцией, не создавая вручную отдельный класс с __enter__ и __exit__. Код до yield соответствует входу в контекст, а код после yield — выходу из него.

Такой подход уменьшает шаблонный код при реализации управления ресурсами. Однако он сохраняет требования протокола: генератор должен корректно дойти до yield, а затем завершиться после выхода из with.

Постановка проблемы

В примере ошибка возникает во время подготовки контекста, то есть при вызове __enter__, реализованного объектом, созданным contextmanager. До выполнения тела блока дело не доходит, поэтому переменная value не получает значения.

Важно отличать эту ситуацию от исключения внутри тела with: исключение до yield происходит на входе, а исключение после передачи управления генератору может быть обработано его блоком try/except.

Подробное решение

Декоратор @contextmanager превращает генераторную функцию в объект, поддерживающий протокол контекстного менеджера. При входе в with он запускает генератор до первой приостановки на yield.

В данном коде выполнение доходит до raise, поэтому yield не выполняется. Объект контекстного менеджера не может вернуть значение для as value, и исключение распространяется наружу. Строка print(value) не запускается.

Минимальный вариант с корректным освобождением ресурса выглядит так:

from contextlib import contextmanager @contextmanager def resource(): print("open") try: yield "resource" finally: print("close") with resource() as value: print(value)

Если ошибка возникает в теле with, механизм возобновляет генератор исключением в точке yield. Поэтому try/except вокруг yield может обработать ошибку тела, а finally выполнится при выходе. Если же ошибка возникает до yield, такой обработчик вокруг yield её не перехватит: генератор ещё не был доведён до этой точки.

После yield генератор должен завершиться. Если он выдаст второе значение, contextmanager сообщит об ошибке протокола, потому что один вход в контекст должен соответствовать ровно одному yield.

Ситуация из практики

Сервис открывает соединение с внешней системой внутри @contextmanager, но аутентификация выполняется до yield. При ошибке аутентификации приложение должно получить исходное исключение и не выполнять операции с несуществующим соединением.

Можно было бы написать класс с __enter__ и __exit__. Это даёт явный контроль над состоянием и удобно для сложного жизненного цикла, но требует больше шаблонного кода. @contextmanager компактнее, однако ошибки до yield и после него нужно размещать осознанно, иначе обработчик вокруг yield не поймает ошибку инициализации.

Практичное решение — выполнять открытие до try, а освобождение помещать в finally после успешного открытия. Тогда ошибка открытия распространяется без попытки закрыть не созданный ресурс, а уже открытый ресурс освобождается при любом выходе после yield.

Что кандидаты часто упускают

  1. Что произойдёт, если исключение возникнет в finally до yield?

    Оно также прервёт вход в контекст и покинет with. Тело блока не начнётся, поскольку генератор не достиг точки yield. Если одновременно существовала исходная ошибка, новая ошибка из finally может замаскировать её, поэтому код очистки должен быть особенно осторожным.

  2. Можно ли поместить обработчик ошибки тела блока только до yield?

    Нет. Исключение из тела блока передаётся генератору в точке yield, поэтому обработчик должен охватывать сам yield, например конструкцией try: yield ... except .... Код, расположенный только до yield, выполняется при входе и не видит ошибки тела.

  3. Что произойдёт, если генератор завершится без yield из-за обычного return?

    Контекст не будет успешно создан: contextmanager ожидает получить значение на первом yield. Если генератор сразу завершится, вход в with завершится ошибкой протокола, а тело блока не выполнится. Поэтому даже условная ветка должна гарантировать единственный yield при успешном входе.