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

Материал из llmdocs — справочник разработчика
Портал: Python
Генераторы и итераторы
Протокол итератораметоды iter и next, исключение StopIteration
ИтераторыPEP 234 (Python 2.1)
ГенераторыPEP 255 (Python 2.2, 2001)
Генераторные выраженияPEP 289 (Python 2.4)
send и throwPEP 342 (Python 2.5)
yield fromPEP 380 (Python 3.3)
Стандартная библиотекаitertools

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

На итераторах построен цикл for, распаковка, zip, map, enumerate, чтение файлов построчно и обработка потоков данных любого размера.

Протокол итерации

Объект итерируемый (iterable), если у него есть метод __iter__, возвращающий итератор. Итератор имеет метод __next__, который возвращает следующий элемент или выбрасывает StopIteration, когда элементы кончились.[2] Цикл for x in data разворачивается примерно так:

it = iter(data)              # вызывает data.__iter__()
while True:
    try:
        x = next(it)         # вызывает it.__next__()
    except StopIteration:
        break
    ...                      # тело цикла

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

Генераторные функции

Генераторы появились в Python 2.2 (PEP 255, 2001).[1] Функция, содержащая yield, при вызове не выполняет тело, а возвращает объект-генератор. Каждый next() выполняет код до следующего yield:

def read_records(path):
    with open(path, encoding="utf-8") as f:
        for line in f:                       # файл читается построчно
            line = line.strip()
            if line and not line.startswith("#"):
                yield line.split(",")

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

gen = countdown(3)
print(next(gen), next(gen), next(gen))      # 3 2 1
# следующий next(gen) выбросит StopIteration

read_records обработает файл на десятки гигабайт, держа в памяти одну строку. Без генератора пришлось бы писать класс с методами __iter__ и __next__ и вручную хранить состояние.

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

Генераторное выражение похоже на списковое включение, но в круглых скобках и вычисляется лениво (PEP 289):[6]

squares_list = [x * x for x in range(10_000_000)]   # список: сотни мегабайт памяти сразу
squares_gen = (x * x for x in range(10_000_000))    # генератор: байты, значения по запросу
total = sum(x * x for x in range(10_000_000))       # скобки функции можно не дублировать

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

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

import itertools

records = read_records("events.csv")
errors = (r for r in records if r[1] == "ERROR")
messages = (r[2] for r in errors)
for msg in itertools.islice(messages, 10):           # первые 10 ошибок; файл дальше не читается
    print(msg)

Модуль itertools содержит готовые строительные блоки: islice (срез итератора), chain (склейка), groupby (группировка подряд идущих), batched (пакеты по n, Python 3.12), product и permutations (комбинаторика), count и cycle (бесконечные последовательности).[5]

Расширенные возможности

  • yield from (PEP 380, Python 3.3) делегирует выдачу значений вложенному генератору или итерируемому объекту: yield from walk(subdir) в рекурсивном обходе дерева.[3]
  • send(), throw(), close() (PEP 342) превращают генератор в сопрограмму, принимающую значения извне. Исторически на этом были построены первые асинхронные фреймворки, а затем и asyncio; сегодня для асинхронности используют async def и await.
  • Асинхронные генераторы (async def с yield) и async for — для потоков данных, приходящих по сети.
  • Генераторы как контекстные менеджеры — декоратор contextlib.contextmanager превращает генератор с одним yield в менеджер для оператора with.

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

  • Повторное использование исчерпанного итератора. Второй проход по генератору ничего не выдаст — нужно создать его заново или сохранить результат в список.
  • Ленивость скрывает ошибки. Код генератора не выполняется до первого next(), поэтому исключение возникнет позже, чем ожидается, — например, когда файл уже закрыт.
  • len() и индексация для генераторов недоступны: длину можно узнать только пройдя его целиком.
  • Ненужная ленивость. Для маленьких коллекций, которые обходятся несколько раз, список проще и быстрее.

Подробнее о функциональном стиле с итераторами — в руководстве Functional Programming HOWTO.[4]

См. также

Источники

  1. PEP 255 — Simple Generators
  2. PEP 234 — Iterators
  3. PEP 380 — Syntax for Delegating to a Subgenerator
  4. Python — Functional Programming HOWTO
  5. Python — itertools
  6. PEP 289 — Generator Expressions