Программирование PythonКоллекции и итераторыPython-разработчик серверных приложений

Генератор приостанавливается на yield, после чего его закрывают. Как завершится генератор при выполнении эт...

Генератор приостанавливается на yield, после чего его закрывают. Как завершится генератор при выполнении этого кода?

def stream():
    try:
        yield 'данные'
    except GeneratorExit:
        print('закрытие')

generator = stream()
print(next(generator))
generator.close()
print('завершён')
Проходите собеседования с ИИ помощником Hintsage

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

Будут напечатаны данные, затем закрытие, затем завершён. Метод close() не возобновляет генератор обычным выполнением: он выбрасывает исключение GeneratorExit в точке текущей приостановки на yield.

В этом коде исключение перехватывается, после чего функция-генератор завершается. Поэтому close() возвращает управление без ошибки, а следующий print выполняется.

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

Генераторы изначально решали задачу ленивой выдачи последовательности без хранения всех элементов в памяти. Позднее протокол генераторов был расширен операциями send(), throw() и close(), чтобы генератор мог не только выдавать значения, но и участвовать в управляемом жизненном цикле вычисления.

close() нужен прежде всего для корректного завершения приостановленного генератора и выполнения его блока finally. Это особенно важно для генераторов, удерживающих файлы, соединения или другие ресурсы.

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

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

Неверное предположение состоит в том, что close() просто вызывает следующий шаг генератора. На самом деле он прерывает выполнение специальным исключением, поэтому обработчики исключений и блоки finally могут изменить результат завершения.

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

Когда выполнение остановлено на yield, вызов generator.close() фактически инициирует GeneratorExit в этой точке. В примере исключение попадает в блок except, печатает закрытие, а затем функция достигает конца и завершается.

GeneratorExit наследуется от BaseException, а не от обычного Exception. Поэтому обработчик except Exception его не перехватит. Это защищает механизм закрытия от случайного подавления обычными обработчиками ошибок.

После нормального завершения генератора close() обычно возвращает None. Если генератор уже завершён, повторный вызов close() также безопасен и не запускает тело заново.

Генератор не должен выдавать новое значение после получения GeneratorExit. Если обработчик GeneratorExit выполнит yield, Python завершит close() исключением RuntimeError. Правильный шаблон обычно использует finally, а не выдачу дополнительных значений при закрытии:

def stream(): resource = acquire() try: yield from read_items(resource) finally: resource.close()

Для предсказуемого освобождения внешних ресурсов предпочтительнее контекстный менеджер. Автоматическое уничтожение генератора не следует использовать как единственный механизм своевременной очистки: момент сборки мусора зависит от реализации и жизненного цикла объектов.

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

Сервис читает большой файл через генератор и передаёт его потребителю. Потребитель может остановиться после первых строк, поэтому файл должен быть закрыт даже при досрочном прекращении обхода.

Первый вариант — надеяться на сборку мусора. Он прост, но не гарантирует своевременное освобождение дескриптора и плохо подходит для долгоживущих процессов.

Второй вариант — вручную вызывать close() у генератора. Это позволяет явно завершить генератор, но требует дисциплины: закрытие нужно выполнить во всех ветках выхода, включая исключения.

Третий вариант — обернуть ресурс в контекстный менеджер и поместить чтение в try/finally. Этот вариант выбран: он явно связывает время жизни файла с блоком with, а генератор отвечает только за ленивую выдачу данных. В результате ранний выход из обработки не оставляет открытый файл.

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

  1. Что произойдёт, если обработчик GeneratorExit ничего не выбросит, но выполнит yield?

    close() завершится исключением RuntimeError, потому что генератор обязан прекратить выдачу значений после запроса закрытия. Обработчик может освобождать ресурсы, записывать состояние или возвращаться, но не должен продолжать нормальный протокол выдачи.

  2. Выполнится ли блок finally, если в генераторе нет обработчика GeneratorExit?

    Да. GeneratorExit проходит через стек выполнения как исключение, поэтому finally выполняется перед окончательным завершением генератора. Именно поэтому конструкция try/finally является стандартным способом очистки ресурсов в генераторе.

  3. Чем close() отличается от полного исчерпания генератора обычным обходом?

    При обычном исчерпании генератор последовательно выполняет код до конца и завершает итерацию через StopIteration. При close() выполнение прерывается в текущей точке посредством GeneratorExit; код после текущего yield может не выполниться, если он не находится в подходящем finally или обработчике этого исключения. Поэтому закрытие — это управляемая отмена, а не ещё один шаг итерации.