Сколько раз генераторный контекстный менеджер должен передать управление через yield за один вход в with?
За один вход в with генераторный контекстный менеджер должен выполнить ровно один yield. Код до него выполняет подготовку, переданное значение становится результатом as, а код после него выполняет освобождение ресурса.
Если yield не выполнен, вход в контекст завершается ошибкой. Если генератор после первого yield выдаёт ещё одно значение вместо завершения, contextlib также сообщает об ошибке: контекстный менеджер нарушил требуемый протокол.
Обычный контекстный менеджер явно реализует методы __enter__ и __exit__. Это удобно для сложного состояния, но для линейного сценария «подготовить ресурс — передать управление — освободить ресурс» требует дополнительного шаблонного кода.
contextlib.contextmanager преобразует генератор в объект, реализующий этот протокол. Единственная точка yield разделяет подготовку и очистку, поэтому её количество является частью контракта такого генератора.
Генераторный контекстный менеджер должен описывать одну фазу использования ресурса: сначала подготовить его, затем один раз передать управление телу with, после чего завершить работу и освободить ресурс.
Несколько yield создают неоднозначность: непонятно, какое значение передавать переменной после as и когда считать выход из контекста завершённым. Отсутствие yield означает, что тело with фактически не получило корректной точки входа.
При входе contextmanager запускает генератор до первого yield. Значение, выданное этим yield, возвращается из __enter__ и может быть присвоено переменной после as.
После завершения тела with контекстный менеджер возобновляет генератор. При обычном выходе генератор должен завершиться; если он продолжает выполнение до второго yield, возникает RuntimeError. Поэтому yield должен встречаться ровно один раз на один запуск генератора.
Если тело with выбрасывает исключение, оно передаётся внутрь генератора в точку yield. Это позволяет обработать ошибку в try/except и выполнить очистку в finally. Исключение можно подавить только намеренно, не передав его дальше; случайное подавление усложняет диагностику.
Здесь генератор выдаёт ровно одно значение. После выполнения тела with управление возвращается в finally, поэтому очистка выполняется и при нормальном выходе, и при исключении.
Генераторный вариант подходит для линейного сценария с одной фазой входа и выхода. Если нужны несколько независимых операций, сложное состояние экземпляра, повторное использование или разные ветви протокола, явный класс с __enter__ и __exit__ обычно понятнее.
Сервис временно переключает соединение с базой данных в режим транзакции. Рассматривались два варианта: класс-контекстный менеджер с полями соединения и флагом состояния либо генераторная функция с try/finally.
Класс дал бы явное состояние и удобное расширение для вложенных операций, но добавил бы шаблонные методы. Генератор оказался короче и нагляднее, потому что сценарий строго линейный: начать транзакцию, передать соединение коду приложения, затем выполнить откат или фиксацию и закрыть служебное состояние.
Был выбран генераторный вариант с одним yield внутри try. Это гарантировало очистку при исключении и не позволяло случайно моделировать несколько входов в один контекст. Если позже потребовались бы повторное использование или сложные переходы состояния, решение стоило бы заменить классом.
Дополнительный вопрос 1: что произойдёт, если исключение возникнет до yield?
Исключение выйдет из операции входа в контекст. Тело with не начнёт выполняться, а __exit__ обёртки не будет вызван как обычный выход из успешно открытого контекста, потому что вход не завершился.
Ресурсы, захваченные до исключения, должны быть освобождены самим кодом подготовки, например через локальный try/except или вложенный механизм управления ресурсами. Нельзя рассчитывать, что код после yield выполнится: до него управление не дошло.
Дополнительный вопрос 2: зачем обрабатывать исключение тела with около yield?
При исключении внутри тела with оно передаётся генератору в точку yield. Поэтому try/except вокруг yield может выполнить специальную обработку, а finally — гарантированную очистку.
Если обработчик завершится нормально и не пробросит исключение дальше, контекстный менеджер подавит его. Если обработчик повторно выбросит исходное или другое исключение, оно покинет with; это обычно безопаснее для ошибок, которые нельзя считать обработанными.
Дополнительный вопрос 3: почему yield внутри цикла обычно нарушает контракт?
Цикл может привести к нескольким выдачам значения за один запуск генератора. После выхода из тела with обёртка ожидает, что генератор завершится, но получает очередной yield и сообщает о нарушении протокола.
Цикл допустим для подготовки внутренних объектов или очистки, если он не порождает дополнительные значения наружу. Наружу должен быть передан только один результат входа, после чего генератор обязан завершиться.