После вызова функции, содержащей yield, какие действия её тела выполнены до первого получения элемента?

После вызова функции, содержащей yield, какие действия её тела выполнены до первого получения элемента?

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

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

До первого получения элемента тело такой функции не выполняется вовсе. Вызов создаёт объект-генератор, а выполнение начинается только при next(), обходе через for или другом возобновлении генератора.

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

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

Генератор сохраняет позицию выполнения между возобновлениями. Это позволяет писать последовательный код вместо ручной реализации итератора с отдельным состоянием.

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

Важно различать вызов функции-генератора и выполнение её тела. Если внутри тела есть побочные эффекты — например, открытие ресурса, журналирование или проверка данных, — они не произойдут в момент вызова.

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

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

Функция, содержащая yield, при вызове возвращает объект-генератор. До первого возобновления тело функции не начинает исполняться, поэтому не выполняются ни операции до первого yield, ни сам первый yield.

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

def produce(): print("тело началось") yield 10 stream = produce() print("создан") print(next(stream))

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

Исключение, возникшее внутри тела, также обычно возникает при первом или последующем возобновлении, а не при создании объекта-генератора. Это даёт ленивость, но переносит момент обнаружения ошибок к потребителю данных.

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

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

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

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

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

  1. Возникает ли исключение из тела генераторной функции в момент её вызова?

    Нет. Исключение из тела возникает при первом или последующем возобновлении генератора. Исключение при вычислении аргумента вызова — отдельный случай: аргументы вычисляются до вызова и может завершить работу уже на этом этапе.

  2. Что произойдёт, если генератор создать, но никогда не начать его обходить?

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

  3. Можно ли передать ненулевое значение через send() генератору, который ещё не запускался?

    Нет. Ненулевое значение нельзя отправить генератору до его первого yield; обычно это приводит к TypeError. Сначала генератор запускают через next() или send(None), после чего send(value) передаёт значение выражению yield, на котором генератор был приостановлен.