Генераторы и итераторы
| Протокол итератора | методы iter и next, исключение StopIteration |
|---|---|
| Итераторы | PEP 234 (Python 2.1) |
| Генераторы | PEP 255 (Python 2.2, 2001) |
| Генераторные выражения | PEP 289 (Python 2.4) |
| send и throw | PEP 342 (Python 2.5) |
| yield from | PEP 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]