Django-Admin.ru

Генераторы и yield в Python: как работают и зачем нужны

Что такое генератор в Python: функция с yield, next() и StopIteration, генераторные выражения, yield from, send и close, чтение больших файлов и частые ошибки.

Редакция Django-Admin.ru
· 17 мин чтения

Содержание · 14 разделов
  1. Что такое генератор в Python
  2. Как работает yield
  3. yield и return: в чём разница
  4. Как получить значения из генератора
  5. Генератор можно пройти только один раз
  6. Генераторные выражения
  7. Генератор или список: память и скорость
  8. Зачем нужны генераторы: практические задачи
  9. Чтение большого файла построчно
  10. Бесконечные последовательности
  11. Конвейер обработки данных
  12. Обработка пачками
  13. yield from: делегирование другому генератору
  14. Методы send(), throw() и close()
  15. Генераторы в Django, FastAPI и стандартной библиотеке
  16. Частые ошибки
  17. Функцию вызвали, но ничего не произошло
  18. TypeError: генератор не поддерживает len() и индексы
  19. RuntimeError: generator raised StopIteration
  20. ValueError: I/O operation on closed file
  21. SyntaxError: Generator expression must be parenthesized
  22. Если вы искали другой «генератор»
  23. Частые вопросы
  24. Чем генератор отличается от итератора?
  25. Можно ли использовать несколько yield в одной функции?
  26. Как узнать длину генератора?
  27. Как перезапустить генератор?
  28. range() — это генератор?
  29. Что такое асинхронный генератор?
  30. Итог

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

def countdown(n):
    while n > 0:
        yield n
        n -= 1

for x in countdown(3):
    print(x)
# → 3
# → 2
# → 1

Ниже — как выполняется функция с yield, чем генератор отличается от списка, генераторные выражения, практические задачи, yield from, методы send() и close() и частые ошибки. Все примеры проверены на Python 3.12, вывод показан комментарием # →.

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

Под словом «генератор» обычно имеют в виду два связанных объекта:

  • генераторная функция — обычная функция def, в теле которой есть хотя бы один yield;
  • объект-генератор — то, что возвращает вызов такой функции. Именно он выдаёт значения.
def countdown(n):
    while n > 0:
        yield n
        n -= 1

gen = countdown(3)
print(type(countdown))  # → <class 'function'>
print(type(gen))        # → <class 'generator'>
print(gen)              # → <generator object countdown at 0x000002ADF4FEC940>

Объект-генератор — частный случай итератора: объекта, у которого можно запрашивать следующий элемент функцией next(), пока элементы не закончатся. Итераторы также возвращают map(), zip(), enumerate() и файловые объекты, но генератор — самый простой способ написать собственный итератор: не нужен класс с методами __iter__ и __next__, достаточно функции с yield.

Для генераторов используют слово «ленивые»: значение считается в момент запроса, а не заранее. Список из миллиона чисел целиком лежит в памяти, генератор того же миллиона хранит только текущее состояние — значения локальных переменных и место, где функция остановилась.

Важно: «генератор списков» в русскоязычных учебниках — это list comprehension, запись вида [x * 2 for x in items]. Её результат — обычный готовый список. Ленивый аналог с круглыми скобками называется генераторным выражением и разобран ниже.

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

yield отдаёт значение вызывающему коду и приостанавливает функцию. При следующем запросе выполнение продолжается со строки после yield — с теми же значениями локальных переменных. Это хорошо видно, если добавить print между шагами:

def numbers():
    print("Старт")
    yield 1
    print("После первого yield")
    yield 2
    print("Конец функции")

gen = numbers()
print(next(gen))
print(next(gen))
print(next(gen))
Старт
1
После первого yield
2
Конец функции
Traceback (most recent call last):
  File "e03.py", line 11, in <module>
    print(next(gen))
          ^^^^^^^^^
StopIteration

Что здесь произошло:

  1. Вызов numbers() не напечатал «Старт»: тело функции ещё не начало выполняться.
  2. Первый next(gen) выполнил код до первого yield и вернул 1.
  3. Второй next(gen) продолжил с места остановки и дошёл до второго yield.
  4. Третий next(gen) довёл функцию до конца. Значений больше нет, поэтому генератор выбросил исключение StopIteration — сигнал «элементы закончились».

Цикл for перехватывает StopIteration сам и просто завершается, поэтому в обычном коде вы этого исключения не видите. Как устроен цикл, подробно разобрано в статье о цикле for в Python.

yield и return: в чём разница

В одной функции может быть сколько угодно yield — каждый выдаёт очередное значение. return в генераторе тоже допустим: он завершает генератор, а его значение попадает в атрибут value исключения StopIteration.

def first_two(items):
    for item in items:
        yield item
        if item == 2:
            return "достигнута двойка"

gen = first_two([1, 2, 3])
print(next(gen))  # → 1
print(next(gen))  # → 2
try:
    next(gen)
except StopIteration as e:
    print("Генератор завершён:", e.value)  # → Генератор завершён: достигнута двойка

Цикл for и list() это значение игнорируют. Получить его удобно через yield from — пример есть в разделе о делегировании.

Признакreturn в обычной функцииyield в генераторе
Что возвращает вызов функцииготовый результатобъект-генератор, тело ещё не выполнялось
Сколько значений отдаётодносколько угодно, по одному
Что происходит с функциейзавершается, переменные теряютсяставится на паузу, состояние сохраняется
Когда считаются значениясразу, всепо запросу (for, next())
Повторный проход по результатуможно (если это список)нельзя: генератор одноразовый

Как получить значения из генератора

Генератор принимают все конструкции, которые работают с итерируемыми объектами:

def countdown(n):
    while n > 0:
        yield n
        n -= 1

print(list(countdown(4)))        # → [4, 3, 2, 1]
gen = countdown(2)
print(next(gen), next(gen))      # → 2 1
print(next(gen, "пусто"))        # → пусто
a, b, c = countdown(3)
print(a, b, c)                   # → 3 2 1
print(sum(countdown(100)))       # → 5050
  • for — основной способ: перебирает все значения и сам останавливается.
  • next(gen) — берёт одно значение. Второй аргумент next(gen, default) возвращается вместо StopIteration, когда генератор пуст.
  • list(), tuple(), set() — собирают все значения в коллекцию. С бесконечным генератором так делать нельзя: программа зависнет и съест память.
  • sum(), max(), min(), sorted(), "".join(), распаковка — принимают генератор напрямую, без промежуточного списка.

Генератор можно пройти только один раз

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

squares = (n * n for n in range(5))
print(list(squares))  # → [0, 1, 4, 9, 16]
print(list(squares))  # → []
print(sum(squares))   # → 0

Это частая причина «пропавших» данных: генератор передали в одну функцию, она его исчерпала, а вторая получила пустоту. Варианты решения:

  • вызвать генераторную функцию заново — каждый вызов создаёт новый независимый генератор;
  • сохранить результат в список, если данных немного и они нужны несколько раз;
  • сделать класс, у которого __iter__ — генераторная функция: тогда каждый for получает свежий генератор.
class Countdown:
    def __init__(self, n):
        self.n = n

    def __iter__(self):
        n = self.n
        while n > 0:
            yield n
            n -= 1

c = Countdown(3)
print(list(c), list(c))  # → [3, 2, 1] [3, 2, 1]

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

Генераторное выражение (generator expression) — короткая запись генератора в одну строку. Синтаксис как у генератора списков, но в круглых скобках:

squares_list = [n * n for n in range(5)]   # список: все элементы сразу
squares_gen = (n * n for n in range(5))    # генератор: элементы по запросу

Если генераторное выражение — единственный аргумент функции, вторые скобки не нужны. Так его и используют чаще всего — с sum(), any(), max(), sorted() и join():

words = ["яблоко", "Банан", "вишня", "Груша"]

print(", ".join(w.lower() for w in words))      # → яблоко, банан, вишня, груша
print(any(w.startswith("Б") for w in words))    # → True
print(max(len(w) for w in words))               # → 6
print(sorted(w for w in words if w.istitle()))  # → ['Банан', 'Груша']

any() к тому же останавливается на первом True и не перебирает остальные элементы — с генератором лишние значения даже не вычисляются.

Генератор или список: память и скорость

Разница в памяти видна через sys.getsizeof():

import sys

nums_list = [n * n for n in range(1_000_000)]
nums_gen = (n * n for n in range(1_000_000))

print(sys.getsizeof(nums_list))  # → 8448728
print(sys.getsizeof(nums_gen))   # → 200

Список из миллиона элементов занимает около 8 МБ только под ссылки (плюс сами числа), генератор — около 200 байт независимо от длины. Точная цифра зависит от версии: в Python 3.14 тот же генератор весит 208 байт.

По скорости на небольших данных разницы почти нет. На наших замерах в Python 3.12 (timeit) sum() по 1000 квадратов работал одинаково с генератором и со списком, а на 10 млн элементов генератор оказался быстрее — 1,4 с против 2,3 с, потому что не тратил время на выделение памяти под огромный список. Цифры зависят от машины.

ЗадачаЧто выбрать
Нужен готовый список: индексы, len(), несколько проходовгенератор списков [...]
Результат перебирается один раз или сразу передаётся в sum, max, joinгенераторное выражение (...)
Данных много или они не помещаются в памятьгенераторное выражение или функция
Логика не помещается в одну строку: несколько условий, try, состояние между шагамигенераторная функция с yield
Нужен словарь или множествогенератор словаря {k: v ...} или множества

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

Зачем нужны генераторы: практические задачи

Чтение большого файла построчно

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

def read_errors(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            if line.startswith("ERROR"):
                yield line.rstrip()

for line in read_errors("app.log"):
    print(line)
# → ERROR disk full
# → ERROR timeout

Файл закрывается автоматически, когда генератор доходит до конца. Если перебор прервали раньше, файл закроется при удалении генератора или при явном вызове close().

Бесконечные последовательности

Генератору не обязательно заканчиваться. Бесконечный цикл while True с yield внутри безопасен: каждое значение вычисляется только по запросу. Ограничивают перебор через break или функцию itertools.islice():

from itertools import count, islice

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

print(list(islice(fibonacci(), 10)))  # → [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

for n in fibonacci():
    if n > 100:
        break
    last = n
print(last)  # → 89

print(list(islice(count(10, 5), 4)))  # → [10, 15, 20, 25]

itertools.count(start, step) — готовый бесконечный счётчик. В модуле itertools есть и другие ленивые инструменты: cycle, chain, takewhile, pairwise.

Конвейер обработки данных

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

def read_lines(lines):
    for line in lines:
        yield line.strip()

def skip_empty(lines):
    for line in lines:
        if line:
            yield line

def to_int(lines):
    for line in lines:
        yield int(line)

raw = ["10\n", "\n", " 25 ", "7\n", ""]
pipeline = to_int(skip_empty(read_lines(raw)))
print(sum(pipeline))  # → 42

Вместо списка raw можно подставить открытый файл, ответ API или курсор базы данных — остальные этапы не изменятся.

Обработка пачками

Записи в базу, запросы к API и отправку писем часто делают пачками фиксированного размера. В Python 3.12 для этого появился itertools.batched(), для более старых версий хватит короткого генератора:

from itertools import batched

def chunks(items, size):
    batch = []
    for item in items:
        batch.append(item)
        if len(batch) == size:
            yield batch
            batch = []
    if batch:
        yield batch

print(list(chunks(range(7), 3)))   # → [[0, 1, 2], [3, 4, 5], [6]]
print(list(batched(range(7), 3)))  # → [(0, 1, 2), (3, 4, 5), (6,)]

batched() выдаёт кортежи, самописный вариант — списки.

yield from: делегирование другому генератору

yield from iterable выдаёт все значения из другого итерируемого объекта — генератора, списка, range. Это короче, чем цикл for x in other: yield x, и дополнительно возвращает значение return вложенного генератора:

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

def numbers():
    yield from range(1, 3)
    return "числа закончились"

def combined():
    yield from letters()
    result = yield from numbers()
    print("numbers() вернул:", result)
    yield "конец"

print(list(combined()))
numbers() вернул: числа закончились
['a', 'b', 1, 2, 'конец']

Строка с print появилась раньше списка: list() выполнял генератор, и печать сработала посередине перебора.

yield from удобен в рекурсии — например, чтобы «расплющить» вложенные списки любой глубины:

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

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

Кроме значений, yield from передаёт во вложенный генератор вызовы send() и throw().

Методы send(), throw() и close()

yield — не только инструкция, но и выражение: в генератор можно передать значение, и оно станет результатом yield. Для этого служит метод send(). Перед первым send() с данными генератор нужно запустить до первого yield вызовом next(gen) или gen.send(None), иначе Python выбросит TypeError: can't send non-None value to a just-started generator.

def running_average():
    total = 0
    count = 0
    average = None
    while True:
        value = yield average
        total += value
        count += 1
        average = total / count

avg = running_average()
next(avg)
print(avg.send(10))  # → 10.0
print(avg.send(20))  # → 15.0
print(avg.send(60))  # → 30.0
avg.close()
print(next(avg, "генератор закрыт"))  # → генератор закрыт
  • send(value) — передаёт значение и возвращает следующее, выданное генератором.
  • throw(exception) — выбрасывает исключение в точке, где генератор остановился. Генератор может его обработать и продолжить работу.
  • close() — завершает генератор: внутри него в месте паузы возникает GeneratorExit. Блоки finally и with при этом отрабатывают, поэтому файлы и соединения закрываются. Python вызывает close() и сам, когда генератор удаляется сборщиком мусора.

С Python 3.13 close() возвращает значение, если генератор перехватил GeneratorExit и выполнил return:

def collector():
    items = []
    try:
        while True:
            items.append((yield))
    except GeneratorExit:
        return items

gen = collector()
next(gen)
gen.send("a")
gen.send("b")
print(gen.close())  # → ['a', 'b'] в Python 3.13+, None в 3.12

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

Генераторы в Django, FastAPI и стандартной библиотеке

С функциями на yield вы встретитесь и там, где не пишете генераторы сами:

  • contextlib.contextmanager превращает генератор в контекстный менеджер для with: код до yield выполняется при входе в блок, после — при выходе.
  • FastAPI использует зависимости с yield: до yield открывается сессия базы данных, после — закрывается. Пример — в статье о FastAPI и SQLAlchemy. Генератор также можно передать в StreamingResponse, чтобы отдавать ответ по частям.
  • Django принимает итератор в StreamingHttpResponse — так выгружают большие CSV без загрузки всего файла в память. Метод QuerySet.iterator() читает записи из базы порциями, без кэширования всей выборки.
from contextlib import contextmanager
import time

@contextmanager
def timer(label):
    start = time.perf_counter()
    try:
        yield
    finally:
        print(f"{label}: {time.perf_counter() - start:.2f} с")

with timer("расчёт"):
    time.sleep(0.2)
# → расчёт: 0.20 с

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

Функцию вызвали, но ничего не произошло

Вызов генераторной функции только создаёт объект. Пока его не перебрали, тело не выполняется — даже print внутри:

def save_all(items):
    for item in items:
        print("сохраняю", item)
        yield item

save_all([1, 2])
print("ничего не сохранилось")
for _ in save_all([1, 2]):
    pass
# → ничего не сохранилось
# → сохраняю 1
# → сохраняю 2

Та же причина у вывода <generator object ... at 0x...> вместо данных: вы напечатали сам генератор. Оберните его в list() или переберите циклом.

TypeError: генератор не поддерживает len() и индексы

У генератора нет длины и нельзя взять элемент по номеру — значения ещё не вычислены:

gen = (n for n in range(5))
len(gen)  # TypeError: object of type 'generator' has no len()
gen[0]    # TypeError: 'generator' object is not subscriptable

Нужна длина или индекс — преобразуйте в список: items = list(gen). Нужен только первый элемент — next(gen). Нужен срез — itertools.islice(gen, 2, 5).

RuntimeError: generator raised StopIteration

С Python 3.7 исключение StopIteration, которое вылетело внутри генератора, превращается в RuntimeError. Обычно его источник — next() по другому итератору:

def first_items(iterable, n):
    it = iter(iterable)
    for _ in range(n):
        yield next(it)

print(list(first_items([1, 2], 5)))
# RuntimeError: generator raised StopIteration

Чтобы завершить генератор, используйте return:

def first_items(iterable, n):
    it = iter(iterable)
    for _ in range(n):
        try:
            yield next(it)
        except StopIteration:
            return

print(list(first_items([1, 2], 5)))  # → [1, 2]

ValueError: I/O operation on closed file

Генераторное выражение, созданное внутри with, переживает сам блок, а файл к моменту чтения уже закрыт:

def read_upper(path):
    with open(path, encoding="utf-8") as f:
        return (line.upper() for line in f)

lines = read_upper("app.log")
next(lines)  # ValueError: I/O operation on closed file.

Сделайте функцию генераторной — замените return (...) на цикл с yield, как в примере с read_errors() выше. Тогда with закроет файл только после окончания перебора.

SyntaxError: Generator expression must be parenthesized

Без скобок генераторное выражение можно передать, только если это единственный аргумент. При нескольких аргументах нужны свои скобки: sum((x for x in range(3)), 10), а не sum(x for x in range(3), 10).

Если вы искали другой «генератор»

Запрос «генератор на Python» часто означает программу, которая что-то генерирует. Коротко о таких задачах:

  • Генератор списков (list comprehension) — запись [выражение for x in коллекция if условие], результат — обычный список. Чем он отличается от генераторного выражения, показано в таблице выше.
  • Генератор случайных чисел — модуль random: random.randint(1, 6), random.choice(items), random.shuffle(items). Все функции и seed() разобраны в статье о модуле random.
  • Генератор паролей — для паролей и токенов используйте модуль secrets, а не random: secrets.token_urlsafe(16) или secrets.choice(alphabet) в цикле. Готовый мини-проект есть в подборке проектов на Python для начинающих.
  • Генератор тестовых данных — сторонняя библиотека Faker создаёт правдоподобные имена, адреса и даты для заполнения базы.

Частые вопросы

Чем генератор отличается от итератора?

Любой генератор — итератор: у него есть __iter__ и __next__, он работает с next() и for. Обратное неверно: итератор можно написать классом вручную. Генератор — короткий способ создать итератор функцией, плюс у него есть методы send(), throw() и close().

Можно ли использовать несколько yield в одной функции?

Да. Каждый yield выдаёт очередное значение, а выполнение продолжается со следующей строки. yield может стоять в циклах, условиях и блоках try.

Как узнать длину генератора?

Только перебрав его: sum(1 for _ in gen) или len(list(gen)). После этого генератор исчерпан. Если длина нужна заранее, храните данные в списке или считайте количество отдельно.

Как перезапустить генератор?

Никак: исчерпанный генератор нельзя вернуть к началу. Вызовите генераторную функцию ещё раз — получите новый объект. itertools.tee() создаёт несколько независимых копий потока, но хранит в памяти элементы, которые одна копия уже прочитала, а другая — ещё нет.

range() — это генератор?

Нет. range тоже ленивый, но это последовательность: у неё есть len(), индексы и срезы, и её можно перебирать сколько угодно раз. Подробности — в статье о функции range().

Что такое асинхронный генератор?

Функция async def, в которой есть yield. Её перебирают через async for, а между значениями можно ждать сетевые запросы через await. Основы асинхронности — в статье про async и await в Python.

Итог

Генератор — функция с yield, которая возвращает ленивый одноразовый итератор: значения вычисляются по одному, а состояние функции сохраняется между ними. Пишите генераторную функцию, когда логика сложная или данные приходят потоком, и генераторное выражение (...), когда результат сразу уходит в sum, max, any или join. Если данные нужны несколько раз, по индексу или с len(), берите список. Остальные конструкции языка — циклы, функции, comprehensions и исключения — собраны в шпаргалке по синтаксису Python.

  • #Python
  • #Генераторы
  • #yield
  • #Итераторы
  • #Функции
↑ ↓ выборEnter открытьEsc закрыть