Генераторы, yield и yield from

⏱ 40 мин · 🧩 заданий: 5

Генератор — функция, которая умеет приостанавливаться

Если в функции есть yield, вызов не выполняет её тело, а возвращает объект-генератор. Каждый next() выполняет код до следующего yield, отдаёт значение и замораживает функцию вместе со всеми локальными переменными:

def countdown(n):
    print("  старт")
    while n > 0:
        print(f"  отдаю {n}")
        yield n                 # пауза: значение наружу, состояние сохранено
        n -= 1
    print("  конец")

g = countdown(3)
print("генератор создан:", g)
print(next(g))
print(next(g))
print(list(g))                  # добирает остаток

Пройдите пример 👣 Пошагово: видно, как выполнение «прыгает» между функцией и вызывающим кодом, а n сохраняется между вызовами.

Генератор — это итератор: у него есть __iter__ и __next__, а в конце он бросает StopIteration. Значит, его можно использовать в for, list(), sum(), распаковке.

Сравните: итератор-класс и генератор

class SquaresIter:                       # 12 строк
    def __init__(self, n):
        self.i, self.n = 0, n
    def __iter__(self):
        return self
    def __next__(self):
        if self.i >= self.n:
            raise StopIteration
        self.i += 1
        return self.i ** 2

def squares_gen(n):                      # 3 строки, то же поведение
    for i in range(1, n + 1):
        yield i ** 2

print(list(SquaresIter(5)), list(squares_gen(5)))

Генератор — самый простой способ написать итератор. В том числе метод __iter__ контейнера:

class Tree:
    def __init__(self, value, *children):
        self.value, self.children = value, children

    def __iter__(self):                 # обход в глубину генератором
        yield self.value
        for child in self.children:
            yield from child

t = Tree(1, Tree(2, Tree(4)), Tree(3))
print(list(t), sum(t), max(t))

Генераторные выражения

Как списковое включение, но в круглых скобках — и ленивое:

import sys

nums = range(1_000_000)
as_list = [x * x for x in nums]
as_gen = (x * x for x in nums)
print(sys.getsizeof(as_list), "байт у списка")
print(sys.getsizeof(as_gen), "байт у генератора")
print(sum(x * x for x in nums))        # скобки функции можно не дублировать
print(any(x > 10 for x in [1, 50, 3])) # any остановится на первом True

Когда что

Нужно один раз пройти и что-то посчитать (sum, max, any, "".join) — генераторное выражение. Нужно обращаться по индексу, проходить несколько раз, узнать len — список.

Конвейеры

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

log = """INFO старт
ERROR диск заполнен
INFO запрос /
ERROR таймаут базы
ERROR диск заполнен"""

def read_lines(text):
    for line in text.splitlines():
        print(f"  читаю: {line}")
        yield line

def only_errors(lines):
    for line in lines:
        if line.startswith("ERROR"):
            yield line

def messages(lines):
    for line in lines:
        yield line.split(" ", 1)[1]

pipeline = messages(only_errors(read_lines(log)))
print("первая ошибка:", next(pipeline))
from collections import Counter
print(Counter(pipeline))

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

from itertools import islice

def naturals(start=1):
    while True:            # никогда не кончается — и это нормально
        yield start
        start += 1

def take(n, iterable):
    return list(islice(iterable, n))

evens = (n for n in naturals() if n % 2 == 0)
print(take(5, evens))
print(take(3, evens))      # продолжает с того же места

yield from

yield from iterable отдаёт все элементы другого итерируемого объекта — и правильно пробрасывает send, throw, close и возвращаемое значение подгенератора:

def letters():
    yield "a"
    yield "b"
    return "буквы готовы"       # return в генераторе → StopIteration.value

def digits():
    yield from range(3)

def combined():
    result = yield from letters()   # результат return подгенератора
    print(f"  letters вернул: {result!r}")
    yield from digits()

print(list(combined()))

send, throw, close

Генератор умеет не только отдавать, но и принимать значения: yield — это выражение, его результат — то, что передали в send():

def running_average():
    total, count = 0, 0
    average = None
    while True:
        value = yield average        # отдаём среднее, ждём новое число
        total += value
        count += 1
        average = total / count

avg = running_average()
next(avg)                            # «заводим» генератор до первого yield
print(avg.send(10), avg.send(20), avg.send(60))
avg.close()                          # внутри бросается GeneratorExit

На этой механике когда-то были построены корутины asyncio (до async/await). Сегодня send в прикладном коде встречается редко, но о нём спрашивают на собеседованиях.

Генератор и ресурсы

finally в генераторе выполнится, даже если генератор не дочитали — при close() или сборке мусора:

def read_records():
    print("  открываю ресурс")
    try:
        for i in range(100):
            yield i
    finally:
        print("  закрываю ресурс")

g = read_records()
print(next(g), next(g))
g.close()

Частые ошибки

# 1. Генератор одноразовый
gen = (x for x in [1, 2, 3])
print(sum(gen), sum(gen))       # 6 0

# 2. Функция с yield не выполняется при вызове
def save_all(items):
    for item in items:
        print("сохраняю", item)
        yield item
save_all([1, 2])                # ничего не напечатает! генератор создан и выброшен
print("^ ничего не сохранено")

# 3. Позднее связывание в генераторном выражении
data = [1, 2, 3]
g = (x for x in data if x in data)
data = [2, 3]                   # первый for вычислился сразу, условие — лениво
print(list(g))

Вопрос на собеседовании

«Что такое генератор и чем он полезен?» — Функция с yield, которая возвращает итератор и выполняется лениво, приостанавливаясь на каждом yield с сохранением локального состояния. Польза: экономия памяти (данные не хранятся целиком), бесконечные последовательности, конвейеры обработки, простое написание итераторов.

Итог

  • yield превращает функцию в фабрику генераторов; тело выполняется по next() до следующего yield.
  • Генератор — итератор: ленивый и одноразовый.
  • Генераторные выражения (...) экономят память для однократного обхода.
  • yield from делегирует другому итерируемому объекту и получает его return.
  • send передаёт значение внутрь, close завершает генератор, finally освобождает ресурсы.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Напишите генератор chunks(items, size), который выдаёт списки по size элементов из последовательности items; последний кусок может быть короче.

Какие тесты проверяют код (3)
assert list(chunks([1, 2, 3, 4, 5], 2)) == [[1, 2], [3, 4], [5]]
assert list(chunks([], 3)) == []
import types; assert isinstance(chunks([1], 1), types.GeneratorType), "chunks должна быть генератором (yield)"
Задание 2 Программа на Python

Напишите бесконечный генератор primes(), выдающий простые числа 2, 3, 5, 7, 11, … Проверка возьмёт из него первые элементы через itertools.islice.

Какие тесты проверяют код (3)
import types; assert isinstance(primes(), types.GeneratorType), "primes должна быть генератором"
from itertools import islice; assert list(islice(primes(), 10)) == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
from itertools import islice; assert list(islice(primes(), 1000))[-1] == 7919
Задание 3 Программа на Python

Напишите генератор flatten(nested), который «расплющивает» произвольно вложенные списки: flatten([1, [2, [3, [4]], 5]]) → 1, 2, 3, 4, 5. Используйте рекурсию и yield from.

Какие тесты проверяют код (2)
assert list(flatten([1, [2, [3, [4]], 5]])) == [1, 2, 3, 4, 5]
assert list(flatten([[], [[]], 7])) == [7]
Задание 4 Один вариант ответа

Что выведет код?

def gen():
    print("старт")
    yield 1
    print("середина")
    yield 2

g = gen()
print("создан")
print(next(g))
Задание 5 Один вариант ответа

Чем (x * 2 for x in data) отличается от [x * 2 for x in data]?

Войдите, чтобы отмечать пройденные уроки.