📖 Теория: Генераторы

⚡ Кратко

Генератор — функция с yield. При вызове возвращает объект-генератор. Каждый next() выполняет код до следующего yield и запоминает состояние.

Сам вызов функции тело не выполняет — код стартует при первом next() или в цикле for.

Генератор одноразовый: после исчерпания повторный обход даст пустой результат — нужно создать новый генератор.

Что такое генератор

Генератор — это функция, содержащая хотя бы одно ключевое слово yield. При вызове такая функция не выполняет тело сразу, а возвращает объект-генератор. Каждый вызов next() запускает код до ближайшего yield, возвращает значение и приостанавливает выполнение.

Как работает yield

def simple_generator():
    print("Начало")
    yield 1
    print("Продолжение")
    yield 2
    print("Конец")

gen = simple_generator()
print(next(gen))  # Начало → 1
print(next(gen))  # Продолжение → 2
# print(next(gen))  # Конец → StopIteration

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

Вызов генераторной функции ничего не выполняет

Обратите внимание на первую строку примера выше: gen = simple_generator() не напечатала «Начало». Вызов только создаёт объект-генератор; тело функции начинает работать при первом next() или в начале цикла for.

gen = simple_generator()   # тело ещё не выполнялось — вывода нет
print(type(gen))           # <class 'generator'>

print(next(gen))           # только теперь: Начало → 1

Это и называется ленивостью: значения вычисляются по требованию, а не заранее.

Генератор одноразовый

Дойдя до конца, генератор исчерпывается — повторно пройти по нему нельзя:

def my_range(start: int, end: int):
    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]
print(list(my_range(1, 4)))   # [1, 2, 3]
⚠️ Частая ловушка. Если сохранить генератор в переменную и передать его в две функции подряд, вторая получит пустую последовательность. Передавайте либо функцию-фабрику, либо готовый список, если данные нужны дважды.

Класс-итератор или генератор

Генератор и класс с __iter__/__next__ решают одну задачу разными средствами:

СвойствоКласс-итераторГенератор
Создание__iter__() и __next__()функция с yield
Состояниехраним в атрибутах объектаPython сохраняет автоматически
Окончаниевручную raise StopIterationавтоматически при выходе из функции
Размер кодаобычно большеобычно меньше
Дополнительные методылегко добавитьменее удобно
Сложное состояние обходаудобенне всегда удобен

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

Генераторы с параметрами

Генератор может принимать аргументы и использовать их при генерации значений:

def count_up_to(n):
    count = 1
    while count <= n:
        yield count
        count += 1

for number in count_up_to(5):
    print(number)  # 1 2 3 4 5

Бесконечные генераторы

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

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

counter = infinite_counter()
for _ in range(5):
    print(next(counter))  # 1 2 3 4 5

Метод close()

Метод close() принудительно завершает генератор. После этого вызов next() бросит StopIteration:

def sensor_reader():
    for value in [10, 20, 30, 40, 50]:
        yield value

sensor = sensor_reader()
for value in sensor:
    print(value)
    if value >= 30:
        sensor.close()
        break

yield from

Конструкция yield from делегирует генерацию другому итерируемому объекту или генератору:

def letters():
    yield from "ABC"

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


def worker(data):
    for value in data:
        yield value * 2


def main(*sequences):
    for seq in sequences:
        yield from worker(seq)

print(list(main([1, 2], [10, 20])))  # [2, 4, 20, 40]

yield from упрощает вложенные генераторы и передаёт исключения между ними.