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

URL: https://skillaria.top/ru/guides/python/generatory-python

Генератор в 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:
        yield start
        start -= 1

gen = countdown(3)
print(gen)          #
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 разобраны на практике в курсе на примере трекера расходов.

## FAQ

**Можно ли использовать 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?**

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


---
Source: Skillaria.Top (https://skillaria.top)
