Урок курса

Генераторы и yield в Python: как это работает

Python для профессионалов

Генератор в Python выдаёт значения по одному, не создавая всю последовательность заранее. Обычная функция заканчивает работу через return, а функция с yield приостанавливается: локальные переменные и позиция выполнения сохраняются до следующего вызова next().

def squares(limit: int):
    for number in range(limit):
        yield number ** 2

values = squares(3)
print(next(values))  # 0
print(next(values))  # 1
print(list(values))  # [4]

Вызов squares(3) не выполняет тело функции целиком. Он возвращает объект-генератор. Каждый next() продолжает код до следующего yield; после исчерпания функции возникает StopIteration. Цикл for обрабатывает этот протокол автоматически.

Зачем нужна ленивость

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

def non_empty(lines):
    for line in lines:
        cleaned = line.strip()
        if cleaned:
            yield cleaned

with open("events.log", encoding="utf-8") as source:
    for event in non_empty(source):
        process(event)

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

Yield, return и генераторные выражения

return value внутри генератора завершает итерацию; значение попадает в StopIteration.value, но обычный for его не показывает. Для передачи последовательности из другого итератора используют yield from iterable.

Генераторное выражение выглядит как comprehension в круглых скобках: (x * x for x in numbers). Оно тоже вычисляет элементы лениво. Не превращайте генераторы в самоцель: короткий список из десяти элементов проще прочитать и отладить как список.

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

Ленивое выполнение и исчерпание генератора

Вызов функции-генератора создаёт объект, но не выполняет её тело до первого next. Дальше выполнение продолжается с места остановки. После исчерпания тот же объект не начинает вычисление заново — для второго прохода создают новый генератор.

def squares(n):
    for i in range(n):
        yield i*i
g = squares(4)
assert next(g) == 0
assert list(g) == [1,4,9]
assert list(g) == []
assert list(squares(4)) == [0,1,4,9]
print("Генератор исчерпан; новый вызов создаёт новый проход")

Попробуйте сначала посчитать sum(g), а затем передать g в список: второй потребитель ничего не получит. Это частая ошибка при повторном использовании iterator. Ленивость экономит хранение всех результатов, но не убирает стоимость вычислений и память исходного источника. Если нужен случайный доступ и многократные проходы, список может быть подходящим решением.

Продолжите разбор: Контекстный менеджер и оператор with в Python.

Задача 1

Что происходит при выполнении yield внутри функции-генератора?

Выберите один вариант
Без регистрации · результат не сохраняется

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку