💻 Примеры генераторов

⚡ Генератор на одном экране

def countdown(n):
    while n > 0:
        yield n          # приостановка, значение наружу
        n -= 1

gen = countdown(3)
print(next(gen), next(gen), next(gen))  # 3 2 1
print(list(countdown(3)))               # [3, 2, 1]
КонструкцияЧто делаетИтог
yieldотдаёт значение и запоминает местоприостановка функции
next(gen)продолжает до следующего yieldзначение или StopIteration
yield from itделегирует другому итерируемомуплоский поток значений
gen.close()завершает генератор досрочнодальше только StopIteration
Топ-3 ошибки: ждать список от gen() без list() · читать один и тот же генератор дважды · забыть остановку у бесконечного генератора.

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

Пример 1. Вызов не выполняет тело функции

Функция с yield при вызове не запускает код — она возвращает объект-генератор. Тело начинает выполняться только на первом next().

def simple_generator():
    print("Старт")
    yield 1
    print("Середина")
    yield 2
    print("Финиш")


gen = simple_generator()
print(gen)                  # <generator object simple_generator at 0x...>
print(type(gen))             # <class 'generator'>

print(next(gen))
# Старт
# 1
print(next(gen))
# Середина
# 2
try:
    print(next(gen))
except StopIteration:
    print("StopIteration")
# Финиш
# StopIteration

Что происходит: строка gen = simple_generator() не печатает «Старт» — тело откладывается до первого next(). Третий next() допечатывает «Финиш» (код после последнего yield) и бросает StopIteration, потому что новых значений больше нет.

Пример 2. next() со значением по умолчанию

Если ловить StopIteration каждый раз неудобно, у next() есть второй аргумент — значение, которое вернётся вместо исключения.

def limited():
    yield "a"
    yield "b"


g = limited()
print(next(g))                      # a
print(next(g))                      # b
print(next(g, "конец данных"))      # конец данных
print(next(g, "конец данных"))      # конец данных — можно звать сколько угодно раз

Что происходит: после второго yield генератор исчерпан. Без запасного значения третий вызов бросил бы StopIteration; с ним — тихо возвращает переданную строку, и код можно писать без try/except.

Пример 3. Генератор одноразовый

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

def my_range(start, end):
    current = start
    while current < end:
        yield current
        current += 1


generator = my_range(1, 4)
print(list(generator))   # [1, 2, 3]
print(list(generator))   # [] — уже исчерпан

print(list(my_range(1, 4)))   # [1, 2, 3] — новый вызов = новый генератор

Что происходит: второй list(generator) возвращает пустой список — не ошибку, а молча пустоту, поэтому баг легко пропустить. Если данные нужны дважды, создавайте генератор заново или храните результат в списке.

Пример 4. Зачем вообще ленивость: генератор против списка по памяти

Главная причина использовать генератор вместо списка — память. Список хранит все элементы сразу, генератор — только текущее состояние.

import sys

list_comp = [x * x for x in range(1000)]
gen_expr = (x * x for x in range(1000))

print(sys.getsizeof(list_comp))   # 8856
print(sys.getsizeof(gen_expr))    # 208

print(sum(list_comp))   # 332833500
print(sum(gen_expr))    # 332833500 — тот же результат

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

⚠️ Проверить по документации: точные размеры в байтах зависят от версии и сборки Python (см. sys.getsizeof) — важен не абсолютный размер, а то, что он не растёт вместе с длиной последовательности.

Пример 5. Бесконечный генератор и управляемая остановка

Бесконечный генератор — обычный паттерн (счётчики, потоки событий). Останавливать его — обязанность вызывающего кода. itertools.islice берёт ровно нужное число элементов, не трогая остальные.

from itertools import islice


def infinite_counter(start=1):
    while True:
        yield start
        start += 1


counter = infinite_counter()
print(list(islice(counter, 5)))   # [1, 2, 3, 4, 5]
print(next(counter))              # 6 — счётчик не сбросился, продолжает с того же места

Что происходит: islice(counter, 5) читает пять значений и останавливается сама — цикл for _ in range(5) с break из теории делает то же самое вручную. После islice генератор не исчерпан: следующий next() продолжает с 6, потому что состояние хранится внутри самого объекта-генератора.

⚠️ Проверить по документации: itertools.islice выходит за рамки лекции — модуль itertools целиком стоит открыть отдельно: itertools.islice.

Пример 6. close() завершает генератор досрочно

def sensor_reader(values):
    for value in values:
        yield value


sensor = sensor_reader([10, 20, 30, 40, 50])
collected = []
for value in sensor:
    collected.append(value)
    if value >= 30:
        sensor.close()
        break

print(collected)   # [10, 20, 30]

try:
    next(sensor)
except StopIteration:
    print("StopIteration after close")   # StopIteration after close

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

Пример 7. yield from: делегирование и рекурсивный «расплющиватель»

def flatten(nested):
    for item in nested:
        if isinstance(item, list):
            yield from flatten(item)
        else:
            yield item


print(list(flatten([1, [2, [3, 4]], 5])))   # [1, 2, 3, 4, 5]


def letters():
    yield from "AB"     # yield from работает с любым iterable, не только с генератором
    yield from [1, 2]


print(list(letters()))   # ['A', 'B', 1, 2]

Что происходит: yield from flatten(item) рекурсивно раскрывает вложенные списки любой глубины — без него пришлось бы вручную писать вложенный цикл for x in flatten(item): yield x. Второй пример показывает, что источником для yield from может быть строка или список — не обязательно другой генератор.

Пример 8. send(): значения можно передавать внутрь генератора

Генератор — не только источник значений наружу. Через send(value) можно передать значение внутрь в точку, где генератор стоит на yield.

def running_total():
    total = 0
    while True:
        value = yield total
        total += value


acc = running_total()
print(next(acc))       # 0  — запускаем генератор до первого yield
print(acc.send(10))    # 10
print(acc.send(5))     # 15
print(acc.send(-3))    # 12

Что происходит: первый next(acc) обязателен — без него генератор ещё не дошёл до yield, и send() сразу после создания бросит ошибку. Каждый следующий send(value) возвращает value из выражения yield total, прибавляет его к сумме и снова замирает на yield — так строятся простые корутины и накопители состояния.

⚠️ Проверить по документации: send() и throw() — продвинутая техника, выходящая за рамки лекции; см. generator.send.

Пример 9. Генератор против класса-итератора: тот же результат, меньше кода

Класс с __iter__/__next__ и функция-генератор решают одну задачу. Сравнение из теории — вживую.

class RangeIterator:
    def __init__(self, start, end):
        self.current = start
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.end:
            raise StopIteration
        value = self.current
        self.current += 1
        return value


print(list(RangeIterator(1, 4)))   # [1, 2, 3]
print(list(my_range(1, 4)))        # [1, 2, 3] — тот же результат, 6 строк вместо 11

Что происходит: оба варианта дают одинаковый список. Класс явно хранит состояние в self.current и сам поднимает StopIteration; генератор из примера 3 делает то же самое короче — Python сохраняет состояние автоматически. Класс оправдан, когда обходу нужны дополнительные методы или атрибуты помимо перебора.

Пример 10. Конвейер из нескольких генераторов

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

def read_lines(text):
    for line in text.splitlines():
        yield line


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


def numbered(lines):
    for index, line in enumerate(lines, start=1):
        yield f"{index}: {line}"


report = """Alice: 90

Bob: 75

Carol: 88"""

pipeline = numbered(non_empty(read_lines(report)))
for row in pipeline:
    print(row)
# 1: Alice: 90
# 2: Bob: 75
# 3: Carol: 88

Что происходит: ни на одном шаге не строится промежуточный список — значения текут по цепочке генераторов по одному, и пустые строки отфильтровываются до нумерации. Тот же приём годится для чтения большого файла: read_lines достаточно заменить на построчное чтение файла, а non_empty/numbered останутся без изменений.

Пример 11. Ловушка: один и тот же генератор нельзя скормить двум потребителям

Ошибка одноразовости из примера 3 особенно незаметна, когда генератор один раз сохранили в переменную и случайно передали в два места кода.

def make_range():
    return my_range(1, 4)


shared = my_range(1, 4)
first_pass = list(shared)
second_pass = list(shared)
print(first_pass, second_pass)   # [1, 2, 3] []

fresh_first = list(make_range())
fresh_second = list(make_range())
print(fresh_first, fresh_second)   # [1, 2, 3] [1, 2, 3]

Как избежать: если данные нужны больше одного раза, передавайте функцию-фабрику (make_range) вместо готового генератора (shared) — каждый вызов фабрики создаёт свежий объект. Альтернатива — сразу materialize-ить результат в список, если объём данных это позволяет.

Что дальше

Практика на генераторы — в заданиях. Если пример не совпал с ожиданием — сверьтесь с типичными ошибками; тема продолжается в уроке 57.