yield в Python: как работает генератор и чем он отличается от return

yield превращает функцию Python в генератор: она отдаёт значения по одному и продолжает с места остановки.

Разбираем, чем yield отличается от return, сколько памяти генератор экономит на миллионе чисел, как работают yield from и send() и какие пять ошибок встречаются чаще всего.

Статью написал:
Ваня Буявец, продюсер, основатель Checkroi
Ваня Буявец
Основатель Checkroi, продюсер, эксперт в выборе онлайн-курсов
Все 3139 статей автора Подписаться на Телеграм-канал
Одобрено экспертом:
Наташа Буявец, основатель Checkroi, эксперт по онлайн-курсам
Наташа Буявец
Основательница Checkroi, продюсер Youtube-каналов, эксперт по онлайн-курсам
Все 3793 экспертных мнения Подписаться на Телеграм-канал
Обложка: yield в Python: как работает генератор и чем он отличается от return

Ключевое слово yield в Python превращает обычную функцию в функцию-генератор. Такая функция не считает весь результат сразу: она отдаёт одно значение, замирает на строке с yield и продолжает с этого же места, когда у неё попросят следующее. Поэтому генератор хранит в памяти только текущий элемент. Он подходит для больших файлов, потоков данных и бесконечных последовательностей.

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

CheckroiCheckroiПодборка курсов по Python569 курсов • 52 школыСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

Как работает yield: пример с next()

Рой за ноутбуком по одной выдаёт карточки из коробки-генератора

Возьмём функцию обратного отсчёта. Вместо того чтобы собрать числа в список и вернуть его через return, она отдаёт их по одному через yield.

def countdown(n):
    print("Старт")
    while n > 0:
        yield n
        n -= 1
    print("Финиш")

gen = countdown(3)
print(type(gen))
print(next(gen))
print(next(gen))
print(next(gen))
next(gen)

Вывод:

<class 'generator'>
Старт
3
2
1
Финиш
Traceback (most recent call last):
  ...
StopIteration

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

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

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

Вручную next() зовут редко. Обычно генератор перебирают циклом for, который сам вызывает next() и сам ловит StopIteration:

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

for x in countdown(3):
    print(x)

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

Как устроен сам цикл с условием, подробно показали в разборе цикла while в Python.

Чем yield отличается от return

Частый вопрос на собеседованиях звучит так: каким ключевым словом заменить return, чтобы функция стала генератором? Ответ: yield. Но разница между ними больше, чем одно слово.

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

Разницу в памяти легко проверить. Две функции считают квадраты миллиона чисел: первая складывает их в список, вторая отдаёт по одному.

import sys

def squares_list(n):
    result = []
    for i in range(n):
        result.append(i * i)
    return result

def squares_gen(n):
    for i in range(n):
        yield i * i

print(sys.getsizeof(squares_list(1_000_000)))
print(sys.getsizeof(squares_gen(1_000_000)))
print(sum(squares_list(1_000_000)) == sum(squares_gen(1_000_000)))
8448728
208
True

Мы запускали код в Python 3.14 на macOS, на другой версии и системе байты могут немного отличаться. Список занял 8 448 728 байт, генератор 208 байт, сумма у обоих одна и та же. Генератор не хранит квадраты, он считает каждый в тот момент, когда sum() его запросил. Такой подход называют ленивыми вычислениями.

Причём реальный разрыв ещё больше. По документации sys.getsizeof() функция учитывает только память самого объекта, а не объектов, на которые он ссылается. Для списка это место под миллион ссылок, сами числа лежат отдельно и в эти 8 МБ не вошли. Как устроены списки и почему они так растут, разбирали в материале о списках в Python.

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

Канал основателя Checkroi Вани Буявца3 700 человек читают мой Телеграм-канал про нейросетиСобрал промпты для Claude Code и ChatGPT, разборы ИИ-инструментов и лайфхаки по продвижению бизнеса в одном местеПрисоединиться

Генератор и итератор: что yield делает за вас

Конвейер, который выпускает по одной детали за раз

Любой генератор относится к итераторам, то есть к объектам, которые отдают значения по одному через next(). Итератор можно написать и без yield, классом с методами __iter__() и __next__(). Вот тот же обратный отсчёт двумя способами:

class Countdown:
    def __init__(self, n):
        self.n = n

    def __iter__(self):
        return self

    def __next__(self):
        if self.n <= 0:
            raise StopIteration
        value = self.n
        self.n -= 1
        return value

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

print(list(Countdown(3)))
print(list(countdown(3)))
gen = countdown(3)
print(iter(gen) is gen)
print(hasattr(gen, "__next__"))
[3, 2, 1]
[3, 2, 1]
True
True

Результат одинаковый, но класс занял 12 строк, а функция 4. В классе вы сами храните состояние в self.n и сами бросаете StopIteration. В генераторе состояние живёт в локальных переменных, а StopIteration Python выбрасывает сам, когда функция заканчивается. Последние две строки вывода показывают, что у генератора есть оба метода итератора.

Классы и методы вроде __init__ подробно разобрали в статье про ООП в Python. Итератор-класс пишут, когда у объекта кроме перебора есть другие методы и данные. Для простой последовательности хватает yield.

Где yield полезен на практике

Генераторы нужны там, где данных много или их поток не кончается. Три типовых случая.

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

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

with open("app.log", "w", encoding="utf-8") as f:
    f.write("INFO старт\nERROR нет связи с базой\nINFO повтор\nERROR таймаут\n")

def read_lines(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            yield line.rstrip("\n")

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

for line in only_errors(read_lines("app.log")):
    print(line)
ERROR нет связи с базой
ERROR таймаут

Первые две строки только создают тестовый файл, в работе у вас будет свой. Как открывать файлы и зачем нужен with, рассказали в инструкции как открыть файл в Python. Тот же приём работает с таблицами: построчное чтение CSV-файлов через модуль csv тоже ленивое.

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

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

from itertools import 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]

Цикл while True внутри генератора не зависает: после каждого yield функция ждёт, пока её позовут снова. Функция islice() берёт первые десять значений и останавливается. Без неё list(fibonacci()) съел бы всю память, поэтому бесконечный генератор всегда перебирают с ограничением или с break внутри цикла.

Генераторное выражение вместо функции

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

squares = (i * i for i in range(5))
print(squares)
print(sum(squares))
print(sum(squares))
<generator object <genexpr> at 0x107465cb0>
30
0

Адрес в первой строке у вас будет другим. Важнее третья строка: второй sum() вернул 0, потому что генератор уже отдал все значения. Это одна из ошибок, о которых ниже.

CheckroiCheckroiПодборка курсов для Python-разработчиков85 курсов • 33 школыСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

yield from: как передать работу другому генератору

Конструкция yield from появилась в Python 3.3 по PEP 380. Она отдаёт наружу все значения другого генератора или любого перебираемого объекта, и не нужно писать для этого отдельный цикл for с yield внутри.

Пример: развернуть вложенный список любой глубины в плоский.

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, 6]]])))
[1, 2, 3, 4, 5, 6]

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

У yield from есть и вторая работа: он забирает значение, которое генератор вернул через return. В генераторе return тоже разрешён, но его значение не попадает в перебор:

def numbers():
    yield 1
    yield 2
    return "готово"

def wrapper():
    result = yield from numbers()
    print("Генератор вернул:", result)

print(list(wrapper()))
Генератор вернул: готово
[1, 2]

В список попали только 1 и 2. Строка «готово» досталась переменной result: по справочнику языка, когда вложенный генератор заканчивается, его возвращённое значение становится значением выражения yield from.

send(): как передать значение внутрь генератора

Генератор умеет не только отдавать значения, но и принимать их. Метод send() добавлен в Python 2.5 по PEP 342. По справочнику языка, если генератор продолжили через send(), выражение yield получает переданное значение, а при обычном next() оно равно None.

Так можно считать скользящее среднее, не храня все числа:

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

avg = averager()
next(avg)
print(avg.send(10))
print(avg.send(20))
print(avg.send(60))
10.0
15.0
30.0

Строка next(avg) перед первым send() обязательна. Она доводит генератор до первого yield, где тот и ждёт значения. Без неё Python не даст отправить число:

TypeError: can't send non-None value to a just-started generator

В прикладном коде send() встречается нечасто, но без него трудно понять, как в Python устроены сопрограммы.

Ваня БуявецКанал основателя Checkroi Вани БуявцаЗабирайте промпты и обучение по нейросетям в моём Телеграм-каналеБольше 3 700 человек уже применяют Claude Code, ChatGPT и другие нейросети в работе, учёбе, бизнесе и жизниПерейти в канал

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

Рой с чек-листом уверенно перешагивает ловушки в коде

Большинство проблем с генераторами сводится к пяти ситуациям.

Генератор пуст при втором обходе

gen = countdown(3)
print(list(gen))
print(list(gen))
[3, 2, 1]
[]

Причина: генератор одноразовый, после первого прохода он исчерпан.

Решение: вызовите функцию заново, list(countdown(3)), или сохраните результат в список, если он нужен несколько раз.

Функцию вызвали, а она ничего не делает

def greet():
    print("Привет!")
    yield

greet()
print("Ничего не напечаталось")
next(greet())
Ничего не напечаталось
Привет!

Причина: вызов функции с yield только создаёт генератор. Тело выполнится, когда генератор начнут перебирать.

Решение: переберите результат через for, list() или next(). Если перебирать нечего и функция просто что-то делает, yield в ней не нужен.

TypeError: object of type ‘generator’ has no len()

Причина: генератор не знает, сколько значений отдаст, пока их не посчитает, поэтому у него нет длины и индексов.

Решение: если длина нужна, превратите генератор в список: len(list(gen)). Помните, что после этого генератор исчерпан.

RuntimeError: generator raised StopIteration

def broken():
    yield 1
    raise StopIteration

print(list(broken()))
RuntimeError: generator raised StopIteration

Причина: по PEP 479 с Python 3.7 исключение StopIteration, выброшенное внутри генератора, заменяется на RuntimeError. Старые примеры из интернета так завершали генератор, и теперь они падают.

Решение: чтобы закончить генератор, используйте return.

SyntaxError: ‘yield’ inside list comprehension

result = [(yield x) for x in range(3)]

Причина: с Python 3.8 yield запрещён внутри списковых включений и генераторных выражений, это записано в справочнике языка.

Решение: если нужен генератор, напишите генераторное выражение без yield, (x for x in range(3)), или обычную функцию с циклом.

Где освоить Python системно

Генераторы закрывают одну тему из многих. Чтобы уверенно писать программы, нужны функции, классы, работа с файлами и исключениями, тесты и практика на задачах с проверкой. Начать можно с подборки задач по Python для начинающих, а системно этому учат курсы с наставником. Ниже онлайн-курсы по Python из нашего каталога, все варианты собраны на странице курсов Python.

КурсШколаСтоимость со скидкойВ рассрочкуДлитель­ностьОбзор курса от Checkroi
Профессия «Python-разработчик»
Перейти на сайт курса
SkillboxSkillbox157 335 ₽5987 ₽/мес.10 месяцевОбзор курса
Fullstack-разработчик на Python
Перейти на сайт курса
НетологияНетология137 000 ₽7125 ₽/мес.21 месяцОбзор курса
ДО Профессия Python-разработчик
Перейти на сайт курса
GeekBrainsGeekBrains149 001 ₽3167 ₽/мес.10 месяцевОбзор курса
Автоматизированное тестирование на Python
Перейти на сайт курса
SkillboxSkillbox122 226 ₽4108 ₽/мес.9 месяцевОбзор курса
Профессия Python-разработчик
Перейти на сайт курса
SkillFactorySkillFactory140 040 ₽3890 ₽/мес.10 месяцевОбзор курса

Больше программ — в полном каталоге курсов по Python

Часто задаваемые вопросы

Что делает yield в Python?

Отдаёт значение из функции и приостанавливает её на этой строке. Следующий вызов next() продолжает выполнение с того же места, локальные переменные сохраняются. Функция с yield при вызове возвращает генератор, а не результат.

Каким ключевым словом заменить return, чтобы функция стала генератором?

Словом yield. Достаточно одного yield в теле функции, и Python будет возвращать при её вызове объект-генератор.

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

Можно. return в генераторе завершает его, а возвращённое значение не попадает в перебор: его можно получить через yield from или из атрибута value у исключения StopIteration.

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

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

Что такое yield return?

Это конструкция языка C#, в Python её нет. В C# yield return указывает следующее значение итератора, в Python ту же работу делает просто yield, а return в генераторе только завершает его.

Сколько раз можно перебрать генератор?

Один. После первого прохода генератор исчерпан, повторный for или list() вернёт пустой результат. Чтобы перебрать снова, вызовите функцию-генератор ещё раз.

Читайте Checkroi первым в Google
Добавьте Checkroi в избранные источники — и наши разборы курсов и обзоры школ будут показываться выше в вашей выдаче Google.
Оставить комментарий
0 комментариев
Форма комментария

Оставьте комментарий

Напишите, что думаете. Нам важно ваше мнение!