Модуль 7 · Итераторы и генераторы

Справочник по Python · модуль 7 · итераторы и генераторы

Глава 7.1

7.1

Итераторы
в Python

Итерируемый объект умеет создать средство перебора, а итератор хранит текущую позицию и выдаёт следующее значение через next(). На этом протоколе работают циклы for и многие встроенные функции.

Категория
Модель перебора
Уровень
Продвинутый
Связанные темы
for, range, map, filter, генераторы
Зачем это знать
Понимать ленивые вычисления и одноразовый перебор

§1Основная идея

Итератор умеет выполнить две операции: выдать следующее значение и сообщить, что значений больше нет. Он не обязан хранить все будущие элементы сразу.

Схемаодин шаг за вызов
источник → iter(источник) → итератор
                              │
                  next() → значение
                  next() → значение
                  next() → StopIteration

Такое устройство разделяет две разные вещи: что перебирается и где находится перебор. Данные остаются в источнике, а позиция хранится в отдельном объекте — поэтому один список можно одновременно проходить в двух местах программы, не мешая друг другу.

§2Ручной перебор через iter() и next()

Функция iter() получает из источника итератор, функция next() запрашивает у него очередное значение.

main.pyтри последовательных шага
numbers = [10, 20, 30]
iterator = iter(numbers)
print(next(iterator))
print(next(iterator))
print(next(iterator))
Выводпозиция движется вперёд
10
20
30

Три одинаковых вызова дают разные результаты — значит, между ними что-то меняется. Меняется состояние итератора: он помнит, что уже отдал, и назад не возвращается.

§3Когда элементы заканчиваются

Следующий вызов после последнего элемента возбуждает StopIteration. Функция next(iterator, default) вместо исключения возвращает запасное значение.

main.pyбезопасное чтение конца
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Повторный перебор

Список может создавать новый итератор для каждого цикла. Сам итератор после чтения не возвращается в начало.

main.pyсписок повторяем, итератор исчерпываем
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Свой итератор

Раз протокол состоит из двух методов, его можно реализовать самому. Класс ниже считает от заданного числа вниз.

main.pyобъект, который умеет перебираться
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=' ')
Выводцикл for принял чужой объект как родной
3 2 1

Три обязательных детали: __next__() возвращает очередное значение, изменяет состояние и возбуждает StopIteration, когда значения кончились. Классы подробно разбираются в модуле 9; здесь важно другое — как только объект реализует протокол, он работает со всем языком сразу: с for, с list(), с sum(), с распаковкой.

Забегая вперёд: почти всё то же самое даёт генераторная функция из главы 7.2 — в четыре строки вместо двенадцати.

§9range — итерируемый объект, но не итератор

range компактно хранит границы числовой последовательности и создаёт отдельный итератор. Поэтому его можно перебирать многократно.

main.pyитератор создаётся явно
numbers = range(3)
iterator = iter(numbers)
print(next(iterator))
print(list(numbers))
print(list(numbers))
Выводrange доступен снова
0
[0, 1, 2]
[0, 1, 2]

Продвижение iterator никак не отразилось на самом range: у них разные роли. range(1000000) при этом не хранит миллион чисел — только начало, конец и шаг, а числа вычисляет на ходу.

§10Встроенные функции, возвращающие итераторы

main.pyzip и enumerate читаются по одному разу
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Одноразовость: главная ловушка

Отсюда следует ошибка, на которую натыкаются все. Итератор, однажды пройденный до конца, остаётся пустым — а выглядит по-прежнему.

main.pyвторой проход не даёт ничего
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 тоже участвует в этом протоколе: он является итератором и перебирается по строкам.

main.pyфайл возвращает сам себя
file = open("demo.txt", encoding="utf-8")
print(iter(file) is file)
file.close()
Выводпризнак настоящего итератора
True

Поэтому for line in file: не загружает файл в память целиком — и поэтому же второй такой цикл по тому же открытому файлу не даст ни одной строки: позиция уже в конце.

§13Изменение коллекции при переборе

Итератор хранит позицию, а не копию данных. Если менять коллекцию во время перебора, позиция «съезжает».

main.pyудаление во время цикла
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() сами возвращают итераторы, их можно соединять — и ни один элемент не будет вычислен раньше, чем понадобится.

main.pyтри звена, ни одного промежуточного списка
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)) вызовет TypeErrornext(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__(). Главное практическое следствие: итератор одноразов и не знает своей длины, поэтому результат, нужный дважды, сохраняют в список, а перебираемую коллекцию не изменяют.

Глава 7.1 · Итераторы · Справочник по Python Автор: Макаров Максим Николаевич