Справочник по Python · модуль 7 · итераторы и генераторы
Глава 7.1
Итераторы
в Python
Итерируемый объект умеет создать средство перебора, а итератор хранит текущую позицию и выдаёт следующее значение через next(). На этом протоколе работают циклы for и многие встроенные функции.
- Категория
- Модель перебора
- Уровень
- Продвинутый
- Связанные темы
- for, range, map, filter, генераторы
- Зачем это знать
- Понимать ленивые вычисления и одноразовый перебор
§1Основная идея
Итератор умеет выполнить две операции: выдать следующее значение и сообщить, что значений больше нет. Он не обязан хранить все будущие элементы сразу.
источник → iter(источник) → итератор │ next() → значение next() → значение next() → StopIteration
Такое устройство разделяет две разные вещи: что перебирается и где находится перебор. Данные остаются в источнике, а позиция хранится в отдельном объекте — поэтому один список можно одновременно проходить в двух местах программы, не мешая друг другу.
§2Ручной перебор через iter() и next()
Функция iter() получает из источника итератор, функция next() запрашивает у него очередное значение.
numbers = [10, 20, 30] iterator = iter(numbers) print(next(iterator)) print(next(iterator)) print(next(iterator))
10 20 30
Три одинаковых вызова дают разные результаты — значит, между ними что-то меняется. Меняется состояние итератора: он помнит, что уже отдал, и назад не возвращается.
§3Когда элементы заканчиваются
Следующий вызов после последнего элемента возбуждает StopIteration. Функция next(iterator, default) вместо исключения возвращает запасное значение.
iterator = iter([10]) print(next(iterator, "конец")) print(next(iterator, "конец"))
10 конец
Обратите внимание: конец последовательности сообщается именно исключением, а не особым значением вроде None. Так None остаётся полноправным элементом — итератор может его выдать, и это не будет означать завершения.
§4Как цикл for использует протокол
Упрощённо цикл сначала вызывает iter(), затем повторяет next() и сам завершает работу при StopIteration.
iterator = iter(iterable) while True: try: item = next(iterator) except StopIteration: break # тело цикла for
В обычной программе этот код писать не нужно: for делает всё автоматически и яснее показывает намерение. Но из схемы видно, почему цикл работает одинаково со списком, строкой, словарём и файлом: ему безразличен источник, важно лишь, чтобы тот отвечал на iter().
Тот же протокол стоит и за распаковкой из главы 1.6: запись a, b, c = источник тоже запрашивает значения по одному.
§5Iterable и iterator
| Понятие | Что умеет | Примеры |
|---|---|---|
| итерируемый объект | создаёт итератор через iter() | список, строка, словарь, множество, range |
| итератор | хранит состояние перебора и отвечает на next() | результаты iter(list), map(), filter(), zip() |
Каждый итератор является итерируемым: iter(iterator) возвращает его самого. Но не каждый итерируемый объект является итератором: next([1, 2]) вызывает TypeError. Русские названия помогают запомнить разницу: «то, что можно перебирать» и «то, что перебирает».
§6Повторный перебор
Список может создавать новый итератор для каждого цикла. Сам итератор после чтения не возвращается в начало.
numbers = [1, 2, 3] print(list(numbers)) print(list(numbers)) iterator = iter(numbers) print(list(iterator)) print(list(iterator))
[1, 2, 3] [1, 2, 3] [1, 2, 3] []
Пустой список в последней строке — не ошибка и не потеря данных: исходный numbers цел. Исчерпался лишь итератор, дошедший до конца; вернуть его в начало нельзя, можно только создать новый.
§7Протокол итератора
На уровне методов итерируемый объект предоставляет __iter__(). Итератор предоставляет __next__(), а его __iter__() возвращает сам итератор. В пользовательском коде эти методы обычно вызывают через функции iter() и next().
| Вызов | Что происходит на самом деле |
|---|---|
iter(obj) | obj.__iter__() |
next(it) | it.__next__() |
for x in obj: | iter(), затем next() до StopIteration |
Именно поэтому for не спрашивает, какой перед ним тип: достаточно, чтобы объект отвечал на эти два метода. Такой подход называют протоколом — договорённостью о наборе методов вместо проверки типа.
§8Свой итератор
Раз протокол состоит из двух методов, его можно реализовать самому. Класс ниже считает от заданного числа вниз.
class Countdown: def __init__(self, start): self.current = start def __iter__(self): return self def __next__(self): if self.current <= 0: raise StopIteration self.current -= 1 return self.current + 1 for value in Countdown(3): print(value, end=' ')
3 2 1
Три обязательных детали: __next__() возвращает очередное значение, изменяет состояние и возбуждает StopIteration, когда значения кончились. Классы подробно разбираются в модуле 9; здесь важно другое — как только объект реализует протокол, он работает со всем языком сразу: с for, с list(), с sum(), с распаковкой.
Забегая вперёд: почти всё то же самое даёт генераторная функция из главы 7.2 — в четыре строки вместо двенадцати.
§9range — итерируемый объект, но не итератор
range компактно хранит границы числовой последовательности и создаёт отдельный итератор. Поэтому его можно перебирать многократно.
numbers = range(3) iterator = iter(numbers) print(next(iterator)) print(list(numbers)) print(list(numbers))
0 [0, 1, 2] [0, 1, 2]
Продвижение iterator никак не отразилось на самом range: у них разные роли. range(1000000) при этом не хранит миллион чисел — только начало, конец и шаг, а числа вычисляет на ходу.
§10Встроенные функции, возвращающие итераторы
pairs = zip(["Анна", "Борис"], [5, 4]) numbered = enumerate(["Python", "SQL"], start=1) print(list(pairs)) print(list(numbered))
[('Анна', 5), ('Борис', 4)]
[(1, 'Python'), (2, 'SQL')]map(), filter(), zip(), enumerate(), reversed() — все они возвращают именно итераторы, а не списки. Ничего не вычисляется, пока результат не запросили: строка map(func, huge_list) выполняется мгновенно независимо от размера списка, потому что не делает ещё ничего.
§11Одноразовость: главная ловушка
Отсюда следует ошибка, на которую натыкаются все. Итератор, однажды пройденный до конца, остаётся пустым — а выглядит по-прежнему.
pairs = zip([1, 2], [3, 4]) print(list(pairs)) print(list(pairs)) values = iter([1, 2, 3]) print(sum(values)) print(sum(values))
[(1, 3), (2, 4)] [] 6 0
Опасен именно второй результат: не ошибка, а тихий ноль и пустой список. Функции вроде sum(), max(), list(), any() потребляют итератор целиком — после них он исчерпан. Если результат нужен дважды, его сохраняют: data = list(pairs).
§12Файл — итератор самого себя
Открытый файл из главы 8.4 тоже участвует в этом протоколе: он является итератором и перебирается по строкам.
file = open("demo.txt", encoding="utf-8") print(iter(file) is file) file.close()
True
Поэтому for line in file: не загружает файл в память целиком — и поэтому же второй такой цикл по тому же открытому файлу не даст ни одной строки: позиция уже в конце.
§13Изменение коллекции при переборе
Итератор хранит позицию, а не копию данных. Если менять коллекцию во время перебора, позиция «съезжает».
numbers = [1, 2, 3, 4] for n in numbers: if n % 2 == 0: numbers.remove(n) print(numbers)
[1, 3]
После удаления двойки элементы сдвинулись влево, а позиция итератора шагнула вправо — четвёрка оказалась пропущена. Словарь в такой ситуации ведёт себя честнее и сразу возбуждает RuntimeError: dictionary changed size during iteration.
Правило: коллекцию, которую перебирают, не изменяют. Перебирают копию (for n in numbers[:]) или, что обычно лучше, строят новый список включением из главы 4.10.
§14Конвейер преобразований
Поскольку map() и filter() сами возвращают итераторы, их можно соединять — и ни один элемент не будет вычислен раньше, чем понадобится.
numbers = range(1, 11) squares = map(lambda x: x * x, numbers) big = filter(lambda x: x > 20, squares) print(next(big)) print(list(big))
25 [36, 49, 64, 81, 100]
Первый next() протянул через всю цепочку ровно столько чисел, сколько потребовалось для одного результата. Если бы каждое звено возвращало список, пришлось бы трижды построить полный набор данных.
§15Зачем значения выдаются по одному
- можно начать обработку до получения всех данных;
- не требуется заранее хранить весь результат;
- можно работать с большими или потенциально бесконечными потоками;
- несколько преобразований соединяются в последовательную обработку.
Экономия памяти появляется только тогда, когда программа не превращает итератор сразу в большой список. Вызов list(map(...)) перечёркивает всю ленивость: данные всё равно окажутся в памяти целиком.
§16Частые ошибки
| Что написано | Что произойдёт | Как правильно |
|---|---|---|
next(numbers) для списка | TypeError: список итерируем, но не является итератором | next(iter(numbers)) |
ещё один next() после конца | возникнет StopIteration | использовать for или второй аргумент next() |
повторно перебирать исчерпанный map или zip | второй проход не даст элементов | создать итератор заново или заранее сохранить список |
считать range итератором | next(range(3)) вызовет TypeError | next(iter(range(3))) |
| удалять элементы из перебираемого списка | часть элементов будет пропущена | перебирать копию или строить новый список |
len(iterator) | TypeError: итератор не знает, сколько значений впереди | материализовать список или считать вручную |
сразу применять list() к огромному потоку | все элементы окажутся в памяти | обрабатывать поток циклом или частями |
§17Проверьте себя
Чем итерируемый объект отличается от итератора?
Первый создаёт средство перебора через iter(), второй хранит текущую позицию и выдаёт значения через next().
Почему список можно пройти дважды, а один объект map обычно нельзя?
Список для каждого прохода создаёт новый итератор; map сам является одноразово продвигающимся итератором.
Как цикл for узнаёт о завершении?
Итератор возбуждает StopIteration, а цикл перехватывает его и заканчивается.
Какие два метода делают объект итератором?
__next__(), выдающий очередное значение, и __iter__(), возвращающий сам объект.
Почему sum(values) во второй раз вернул ноль?
Первый вызов прошёл итератор до конца и исчерпал его, поэтому во второй раз складывать было нечего.
Почему нельзя удалять элементы прямо в цикле for?
Итератор помнит позицию в исходной коллекции. После удаления элементы сдвигаются, и следующий по счёту оказывается пропущен.
§18Шпаргалка
iterator = iter(iterable)next(iterator)next(iterator, default)for item in iterable:__iter__() и __next__()data = list(iterator)Итог главы
Итерируемый объект создаёт итератор через iter(). Итератор хранит состояние, выдаёт следующий элемент через next() и сообщает о конце исключением StopIteration. Цикл for управляет этим протоколом автоматически, а собственный объект включается в него двумя методами — __iter__() и __next__(). Главное практическое следствие: итератор одноразов и не знает своей длины, поэтому результат, нужный дважды, сохраняют в список, а перебираемую коллекцию не изменяют.