Программирование PythonКоллекции и итераторыРазработчик Python, отвечающий за потоковую обработку данных и управление ресурсами

Разберите последствие: что произойдёт при закрытии генератора, если его блок finally пытается выполнить yield?

Разберите последствие: что произойдёт при закрытии генератора, если его блок finally пытается выполнить yield?

Проходите собеседования с ИИ помощником Hintsage

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

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

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

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

Поддержка finally нужна для освобождения ресурсов при нормальном завершении и при досрочном закрытии. Запрет на yield во время закрытия защищает этот протокол от неоднозначного состояния: закрываемый генератор не должен внезапно продолжать выдавать данные.

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

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

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

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

Во время close() в приостановленный генератор передаётся GeneratorExit в точку последнего yield. Блок finally всё равно начинает выполняться, поэтому код очистки внутри него обычно надёжен.

Однако yield в этом пути запрещён. Если генератор выдаст значение вместо завершения, Python поднимет RuntimeError с сообщением о том, что генератор проигнорировал GeneratorExit.

def broken(): try: yield "данные" finally: yield "очистка" stream = broken() print(next(stream)) stream.close() # RuntimeError

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

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

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

Сервис читает большой файл генератором и хочет в конце выдать статистику обработки. Вариант с yield статистики из finally прост, но небезопасен: при остановке чтения после первой ошибки или при отмене задачи close() вызовет RuntimeError.

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

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

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

  1. Работает ли yield в finally, если генератор завершается обычным исчерпанием?

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

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

  1. Выполнится ли finally, если закрыть генератор до первого next()?

Нет. До первого next() тело генератора ещё не начало исполняться, поэтому его try и finally не были активированы. Вызов close() у такого генератора не запускает тело и обычно завершается без эффекта.

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

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

Результат тот же: close() получит RuntimeError, потому что генератор выдал значение после сигнала закрытия. Сам факт явного перехвата GeneratorExit не отменяет требования завершить генератор.

Корректный обработчик может выполнить очистку и завершиться через return, но обычно дополнительный перехват не нужен: finally позволяет освободить ресурсы без изменения протокола закрытия.