Вызов функции с yield не выполняет её тело немедленно. Какой вывод даст программа и каким механизмом это об...

Вызов функции с yield не выполняет её тело немедленно. Какой вывод даст программа и каким механизмом это объясняется?

def f():
    print("start")
    yield 1
    print("end")

g = f()
print(type(g).__name__)
print("created")
print(next(g))
Проходите собеседования с ИИ помощником Hintsage

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

Программа выведет:

generator created start 1

Вызов f() сразу создаёт объект-генератор, но тело функции ещё не запускает. Выполнение начинается только при вызове next(g) и приостанавливается на yield 1.

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

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

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

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

Неверное ожидание состоит в том, что вызов f() выполнит print("start") и подготовит результат. В действительности он только создаёт вычисление, которое ещё не началось.

Это важно учитывать при отладке, обработке файлов и проектировании API. Ошибка в предположении о моменте выполнения может привести к тому, что исключение возникнет не при создании генератора, а значительно позже — при итерации.

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

Наличие yield делает f генераторной функцией. При вызове f() Python возвращает объект типа generator; инструкции тела до первого yield в этот момент не исполняются.

print(type(g).__name__) выводит generator, затем обычный print("created") выводит created. Только next(g) возобновляет выполнение: сначала печатается start, затем значение 1 возвращается вызывающему коду.

После yield состояние генератора сохраняется. Следующий вызов next(g) продолжил бы выполнение с инструкции print("end"), напечатал бы end, а затем завершил генератор исключением StopIteration.

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

Минимальный пример ленивого чтения:

def numbers(): for value in range(3): print("создано", value) yield value for item in numbers(): print("получено", item)

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

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

Сервису нужно обработать многогигабайтный журнал. Вариант со list(file) прост, но загружает все строки в память и может привести к её исчерпанию. Вариант с обычной функцией, возвращающей готовый список, имеет тот же недостаток.

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

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

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

  1. Что произойдёт при вызове next(g) после завершения генератора?

    Будет возбуждён StopIteration. Это нормальный сигнал окончания итерации, поэтому цикл for перехватывает его автоматически и завершает перебор без ошибки.

    Если генератор завершился оператором return result, значение доступно как StopIteration.value, но обычный цикл его не предоставляет:

    def f(): return 42 g = f() try: next(g) except StopIteration as error: print(error.value)
  2. Когда выполняется код в finally генератора?

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

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

  3. Чем отличаются next(g), g.send(value) и g.throw(error)?

    next(g) возобновляет генератор, передавая внутрь значение None. send(value) возобновляет его так, что выражение yield получает value; передать ненулевое значение можно только уже запущенному генератору.

    throw(error) возобновляет выполнение с возбуждением указанного исключения в точке текущего yield. Эти операции позволяют строить сопрограммы, но усложняют управление состоянием; для обычного последовательного перебора достаточно for или next().