Основы

Генераторы в Python: yield, итераторы и генераторы списков и словарей

Обновлено: · Skillaria.Top

Генератор в Python — это объект, который выдаёт значения по одному и по запросу, не храня их все в памяти; его создают функцией с ключевым словом yield или генераторным выражением в круглых скобках. Генераторы списков, словарей и множеств (comprehensions) — короткая запись для сборки готовой коллекции: [x * 2 for x in xs], {k: v for k, v in pairs}, {x for x in xs}.

Генератор в Python — это объект, который выдаёт значения по одному, только когда их попросят, и не держит всю последовательность в памяти. Создают его двумя способами: функцией с ключевым словом yield или генераторным выражением в круглых скобках. Рядом живут «генераторы списков», словарей и множеств — comprehensions, короткая запись для сборки готовой коллекции в одну строку. Разберём, как работает python yield, чем генератор отличается от итератора и как не превратить comprehension в нечитаемую строку.

Итераторы: на чём стоит цикл for

Чтобы понять генераторы, нужно сначала разобраться с итераторами. Итерируемый объект (iterable) — то, по чему можно пройти циклом: список, строка, словарь, файл. Итератор — объект, который помнит текущую позицию и по вызову next() отдаёт следующий элемент, а когда элементы кончились, выбрасывает StopIteration.

nums = [10, 20, 30]
it = iter(nums)      # получить итератор из списка
print(next(it))      # 10
print(next(it))      # 20
print(next(it))      # 30
next(it)             # StopIteration

Цикл for делает ровно это: вызывает iter(), потом next() до StopIteration. Свой итератор можно написать классом с методами __iter__ и __next__:

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

    def __iter__(self):
        return self

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

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

Работает, но много шаблонного кода. Генераторы делают то же самое в три строки.

Что такое генератор в Python и как работает yield

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

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

gen = countdown(3)
print(gen)          # <generator object countdown at 0x...>
print(next(gen))    # 3
print(list(gen))    # [2, 1] — продолжили с места остановки

Когда функция доходит до конца (или до return), генератор сам выбрасывает StopIteration, и цикл for аккуратно завершается. Отсюда главное отличие yield от return: return завершает функцию и возвращает одно значение, yield приостанавливает её и может отдать сколько угодно значений.

Генератор и итератор: отличия

Любой генератор — итератор: у него есть __iter__ и __next__. Но не любой итератор — генератор. Разница в способе создания и в возможностях.

Итератор (класс)Генератор
Как создатькласс с __iter__ и __next__функция с yield или (x for x in ...)
Состояниевручную в атрибутахсохраняется автоматически в локальных переменных
StopIterationвыбрасываете самипоявляется сам при выходе из функции
Дополнительночто напишетеsend(), throw(), close()

Общее у них одно важное свойство: пройти можно только один раз. Повторный for по исчерпанному генератору ничего не выдаст — нужно создать новый.

Бесконечный генератор и чтение больших файлов

Генератор не обязан заканчиваться. Бесконечная последовательность безопасна, пока вы берёте из неё ограниченное число элементов:

from itertools import islice

def ids():
    n = 1
    while True:
        yield f"ID-{n:04d}"
        n += 1

print(list(islice(ids(), 3)))  # ['ID-0001', 'ID-0002', 'ID-0003']

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

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 "ERROR" in line:
            yield line

for line in only_errors(read_lines("app.log")):
    print(line)

yield from и send

yield from передаёт все значения другого итерируемого объекта, избавляя от вложенного цикла:

def flatten(groups):
    for g in groups:
        yield from g

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

Метод send() отправляет значение внутрь генератора: оно становится результатом выражения yield. Перед первым send генератор нужно «запустить» через next(). Так пишут, например, накопитель среднего:

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

a = averager()
next(a)            # дойти до первого yield
print(a.send(10))  # 10.0
print(a.send(20))  # 15.0

В прикладном коде send встречается редко, но о нём часто спрашивают на собеседованиях.

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

Нет. range — ленивая последовательность: он тоже не хранит все числа, но поддерживает индексацию, len(), проверку in и проход сколько угодно раз. Генератор ничего из этого не умеет.

r = range(5)
print(r[2], len(r), 3 in r)  # 2 5 True
print(list(r), list(r))      # оба раза [0, 1, 2, 3, 4]

Генератор списков: list comprehension

Генератор списков (list comprehension) — запись [выражение for x in коллекция]. Она создаёт новый список и не меняет исходный. Читается слева направо: сначала «что положить», потом «откуда брать».

amounts = [100, 250, 400]

# обычный цикл
with_markup = []
for a in amounts:
    with_markup.append(a * 1.2)

# то же через comprehension
with_markup = [a * 1.2 for a in amounts]
print(with_markup)  # [120.0, 300.0, 480.0]

Несмотря на русское название «генератор списков», результат — обычный готовый список, а не генератор: все элементы вычисляются сразу.

Генератор списка с условием if и if else

Тут путаются чаще всего. Условие-фильтр ставится в конец и отбрасывает элементы. Условие-выбор (тернарный оператор) ставится в начало и превращает каждый элемент в одно из двух значений, ничего не выбрасывая.

amounts = [100, 250, 400]

big = [a for a in amounts if a > 200]                   # [250, 400]
labels = ["много" if a > 200 else "мало" for a in amounts]
# ['мало', 'много', 'много']

# фильтр и выбор вместе
odd_marks = ["чёт" if n % 2 == 0 else "нечёт" for n in range(6) if n > 2]
# ['нечёт', 'чёт', 'нечёт']

Запись [a for a in amounts if a > 200 else 0] — синтаксическая ошибка: else бывает только у условия в начале.

Вложенные генераторы списков и двумерный список

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

groups = [[1, 2], [3], [4, 5]]
flat = [x for g in groups for x in g]
# эквивалентно:
# for g in groups:
#     for x in g:
#         flat.append(x)
print(flat)  # [1, 2, 3, 4, 5]

Двумерный список (матрицу) создают comprehension внутри comprehension. Классическая ошибка — умножение списка: строки окажутся одним и тем же объектом.

bad = [[0] * 3] * 2
bad[0][0] = 1
print(bad)   # [[1, 0, 0], [1, 0, 0]] — обе строки изменились

grid = [[0] * 3 for _ in range(2)]
grid[0][0] = 1
print(grid)  # [[1, 0, 0], [0, 0, 0]]

# транспонирование
m = [[1, 2, 3], [4, 5, 6]]
print([[row[i] for row in m] for i in range(3)])  # [[1, 4], [2, 5], [3, 6]]

Генератор словаря и множества

Тот же приём работает для других коллекций. Фигурные скобки с двоеточием собирают словарь (dict comprehension), без двоеточия — множество (set comprehension).

spent = [("еда", 500), ("такси", 300), ("еда", 200)]

categories = {cat for cat, _ in spent}
print(categories)  # {'еда', 'такси'} — порядок у множества не гарантирован

last = {cat: amount for cat, amount in spent}
print(last)        # {'еда': 200, 'такси': 300}

squares = {n: n ** 2 for n in range(4)}  # {0: 0, 1: 1, 2: 4, 3: 9}
swapped = {v: k for k, v in squares.items()}

Обратите внимание на last: при повторе ключа побеждает последнее значение, сумма 500 потерялась. Если нужно сложить, comprehension не подходит — пишите цикл или используйте collections.Counter / defaultdict.

А генератор кортежей?

Отдельного «tuple comprehension» в Python нет. Круглые скобки дают генераторное выражение, а кортеж получают, передав его в tuple():

t = tuple(n * 2 for n in range(3))  # (0, 2, 4)

Генераторное выражение: не хранить, а выдавать

Если заменить квадратные скобки на круглые, получится генераторное выражение. Оно ничего не считает заранее и выдаёт значения по одному. Для миллиона элементов разница в памяти огромная:

import sys

as_list = [n for n in range(1_000_000)]
as_gen = (n for n in range(1_000_000))
print(sys.getsizeof(as_list))  # около 8 МБ
print(sys.getsizeof(as_gen))   # пара сотен байт

Чаще всего генераторные выражения живут внутри функций-агрегатов. Если генератор — единственный аргумент, лишние скобки можно не писать. any() и all() к тому же останавливаются на первом же ответе и не перебирают остаток.

spent = [("еда", 500), ("такси", 300), ("еда", 200)]

total = sum(a for _, a in spent)            # 1000
has_big = any(a > 400 for _, a in spent)    # True, дальше 500 не смотрит
biggest = max(spent, key=lambda p: p[1])    # ('еда', 500)

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

Область видимости: переменная не утекает

Вопрос, на котором спотыкаются даже опытные:

x = 10
squares = [x for x in range(3)]
print(x)  # 10

Переменная цикла внутри comprehension живёт в своей области и не затирает внешнюю (так в Python 3; в Python 2 было иначе). В обычном цикле for после него x равнялся бы 2. С версии 3.12 (PEP 709) comprehensions встраиваются в окружающую функцию и работают быстрее, но правило видимости осталось прежним.

Распаковка * и ** в comprehensions: новое в Python 3.15 (PEP 798)

Звёздочку для распаковки давно можно писать в вызове функции и в литерале списка, а вот внутри comprehension это было SyntaxError. В Python 3.15 запрет сняли: *элемент означает «вставь сюда все его элементы». Работает в списках, множествах и генераторах, включая асинхронные.

# Python 3.15+
groups = [[1, 2], [3], [4, 5]]
flat = [*g for g in groups]        # [1, 2, 3, 4, 5]
uniq = {*g for g in groups}        # множество
lazy = (*g for g in groups)        # ленивый генератор

Двойная звёздочка делает то же для словарей. Это удобно для слоёных настроек «по умолчанию → файл → флаги»: каждый следующий слой перекрывает ключи предыдущего.

# Python 3.15+
layers = [{"debug": False, "port": 80}, {"port": 8080}, {"debug": True}]
config = {**layer for layer in layers}
# {'debug': True, 'port': 8080}

Две оговорки из курса. Звёздочку нельзя совмещать с условным выражением без скобок: [*w if w else 0 for w in weeks] — ошибка синтаксиса, условие нужно обернуть в скобки. И такой код не запустится на 3.14 и ниже: если библиотека поддерживает старые версии, оставьте двойной for или itertools.chain.from_iterable.

Асинхронные генераторы

Если в функции async def есть yield, получается асинхронный генератор. По нему проходят async for, а внутри можно ждать сеть или диск через await. Есть и асинхронные comprehensions.

import asyncio

async def ticks(n):
    for i in range(n):
        await asyncio.sleep(0.1)
        yield i

async def main():
    result = [i async for i in ticks(3)]
    print(result)  # [0, 1, 2]

asyncio.run(main())

Типичные ошибки

  • Comprehension на три этажа. Два for, два if и тернарник в одной строке никто не прочитает через неделю. Не помещается в строку — пишите обычный цикл: PEP 8 ставит читаемость выше краткости.
  • Повторный проход по генератору. Второй sum(gen) вернёт 0, потому что генератор уже исчерпан.
  • Comprehension ради побочного эффекта. [print(x) for x in xs] строит ненужный список из None. Для действий — цикл for.
  • Ожидание списка от генераторного выражения. (x for x in xs)[0] — TypeError: у генератора нет индексов.

Задачи на генераторы для практики

  1. Напишите генератор fib(), бесконечно выдающий числа Фибоначчи, и возьмите первые 10 через islice.
  2. Одной строкой получите словарь «слово → длина» для слов длиннее трёх букв.
  3. Посчитайте сумму квадратов чётных чисел до миллиона, не создавая список.
  4. Напишите генератор chunks(seq, n), выдающий куски списка по n элементов.

Итог

Итератор отдаёт элементы по одному через next(), генератор — самый простой способ его написать: функция с python yield или выражение в круглых скобках. Генераторы списков, словарей и множеств собирают готовую коллекцию в одну строку; фильтр if ставится в конец, выбор if else — в начало. Нужно экономить память или обработать поток данных один раз — берите генератор, нужен результат многократно — список. Comprehensions, генераторы и новая распаковка из Python 3.15 разобраны на практике в курсе на примере трекера расходов.

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

Можно ли использовать return в генераторе?

Да. return без значения просто завершает генератор, а return со значением кладёт его в атрибут value исключения StopIteration; это значение получает выражение yield from.

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

Передайте его в list(): list(gen). Учтите, что генератор после этого исчерпан, а все значения окажутся в памяти.

Что быстрее: list comprehension или цикл с append?

Обычно comprehension немного быстрее, потому что не вызывает метод append на каждой итерации. Но выбирать стоит по читаемости, разница редко бывает решающей.

Как узнать, что объект — генератор?

Используйте inspect.isgenerator(obj) для объекта или inspect.isgeneratorfunction(f) для функции; проверить, что объект итератор, можно через isinstance(obj, collections.abc.Iterator).

Чем генераторное выражение отличается от list comprehension?

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

Разберите тему на видео

Эта тема подробно разобрана с практикой в курсе «Python 3.15: от новичка до профи».

Перейти к курсу