Итератор в Python

Каждый раз когда ты пишешь for - ты используешь итератор. Пора наконец понять как это работает изнутри

Итератор в Python.md

Содержание


Коротко и чётко

Итератор - это объект, который реализует два метода:

  • __iter__() - возвращает сам себя
  • __next__() - возвращает следующий элемент. Когда элементы закончились - бросает исключение StopIteration

Итерируемый объект (iterable) - объект, у которого есть __iter__(), возвращающий итератор. Это не одно и то же.

Итерируемый объект  - есть __iter__(), возвращает итератор
Итератор            - есть __iter__() и __next__()

Список - итерируемый, но не итератор
iter(список) - уже итератор

Итератор в Python - это объект с двумя методами: __iter__ и __next__.
__next__ возвращает элементы по одному, при исчерпании бросает StopIteration.
Цикл for под капотом вызывает iter() чтобы получить итератор, потом многократно вызывает next() пока не поймает StopIteration.
Главное отличие итератора от итерируемого объекта: список - итерируемый, но не итератор. iter(список) - уже итератор.
Практическая ценность - ленивые вычисления: итератор не держит все данные в памяти, а отдаёт по одному элементу по требованию.


Простыми словами

Представь книгу с закладкой.

Книга - это данные. Все страницы уже существуют, они лежат и ждут. Но ты не читаешь все страницы сразу - ты читаешь по одной, и закладка запоминает, где ты остановился.

Итератор - это и есть закладка. Он знает текущую позицию и умеет делать одно действие: «дай мне следующую страницу».


Теперь важное различие, которое путают чаще всего.

Список - это книга. В ней уже есть все страницы, все данные в памяти. Список итерируемый - по нему можно пройтись. Но сам список не является итератором.

Итератор - это книга с закладкой. Он помнит позицию и выдаёт по одному элементу.

numbers = [1, 2, 3]   # это список - итерируемый объект

it = iter(numbers)    # это итератор - закладка в списке

print(next(it))       # 1  - перевернули страницу
print(next(it))       # 2  - ещё раз
print(next(it))       # 3  - ещё раз
print(next(it))       # StopIteration - страницы кончились

Теперь смотри что происходит под капотом цикла for. Каждый раз когда ты пишешь:

for item in [1, 2, 3]:
    print(item)

Python делает буквально следующее:

1. Вызывает iter([1, 2, 3])     - берёт закладку
2. Вызывает next(iterator)      - читает страницу 1
3. Вызывает next(iterator)      - читает страницу 2
4. Вызывает next(iterator)      - читает страницу 3
5. Вызывает next(iterator)      - ловит StopIteration
6. Цикл заканчивается

Ты никогда не видишь StopIteration - for тихо ловит его сам и останавливается. Но именно так это работает всегда.


Зачем это нужно - ленивые вычисления

Представь, что тебе нужно обработать файл на 10 гигабайт. Если загрузить всё в список - оперативная память закончится. Итератор решает эту проблему: он читает и отдаёт по одной строке, не загружая весь файл.

Список:    [строка1, строка2, строка3, ... строка10000000]  <- всё в памяти сразу
Итератор:  строка1 -> строка2 -> строка3 -> ...             <- по одной, по требованию

Это называется ленивые вычисления (lazy evaluation) - считаем только то, что нужно прямо сейчас.


Код

Как работает итератор - под капотом цикла for

numbers = [1, 2, 3]

# Вот что делает цикл for на самом деле
iterator = iter(numbers)      # iter() вызывает numbers.__iter__()

while True:
    try:
        item = next(iterator) # next() вызывает iterator.__next__()
        print(item)
    except StopIteration:     # элементы кончились - выходим
        break

# Это полностью эквивалентно:
for item in numbers:
    print(item)

Свой итератор с нуля

class Countdown:
    """Итератор обратного отсчёта - от n до 0"""

    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self  # итератор возвращает сам себя

    def __next__(self):
        if self.current < 0:
            raise StopIteration  # сигнал: элементы закончились

        value = self.current
        self.current -= 1
        return value


# Используем как любой питоновский объект
countdown = Countdown(3)

for n in countdown:
    print(n)
# 3
# 2
# 1
# 0

# Или вручную
cd = Countdown(2)
print(next(cd))   # 2
print(next(cd))   # 1
print(next(cd))   # 0
print(next(cd))   # StopIteration

Итератор vs итерируемый объект - ключевое отличие

numbers = [1, 2, 3]

# Список - итерируемый, но НЕ итератор
print(hasattr(numbers, '__iter__'))  # True
print(hasattr(numbers, '__next__'))  # False <- нет __next__, значит не итератор

# iter() создаёт итератор из итерируемого объекта
it = iter(numbers)
print(hasattr(it, '__iter__'))       # True
print(hasattr(it, '__next__'))       # True <- теперь есть __next__

# Важное следствие - список можно итерировать много раз
for n in numbers: print(n)  # 1 2 3
for n in numbers: print(n)  # 1 2 3 - начинает сначала!

# Итератор - только один раз, он запоминает позицию
it = iter(numbers)
for n in it: print(n)  # 1 2 3
for n in it: print(n)  # ничего - итератор уже исчерпан

Ленивый итератор - чтение большого файла

# ПЛОХО для большого файла - всё в памяти сразу
def read_all(filename):
    with open(filename) as f:
        return f.readlines()  # загружает весь файл в список

lines = read_all("huge_file.txt")   # 10 ГБ в RAM


# ХОРОШО - итератор, по одной строке
def read_lazy(filename):
    with open(filename) as f:
        for line in f:        # файл сам является итератором
            yield line        # отдаём по одной строке

# Неважно сколько строк в файле - в памяти одна строка за раз
for line in read_lazy("huge_file.txt"):
    process(line)

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

# map, filter, zip, enumerate - все возвращают итераторы, не списки
numbers = [1, 2, 3, 4, 5]

doubled = map(lambda x: x * 2, numbers)
print(doubled)          # <map object at 0x...> - это итератор!
print(next(doubled))    # 2
print(next(doubled))    # 4

# Чтобы получить список - явно конвертируй
print(list(map(lambda x: x * 2, numbers)))  # [2, 4, 6, 8, 10]

# range тоже не список - это итерируемый объект
r = range(1_000_000)
# Не занимает мегабайты памяти - просто знает начало, конец и шаг

Глубже

Итератор - это одноразовый объект. Как только __next__ бросил StopIteration - итератор исчерпан. Перемотать назад нельзя, начать сначала нельзя. Если нужно пройтись ещё раз - создавай новый итератор через iter(). Это принципиальное отличие от итерируемого объекта: список можно итерировать сколько угодно раз, его итератор - только один.

iter() и next() - это обёртки над дандер-методами. iter(obj) вызывает obj.__iter__(). next(obj) вызывает obj.__next__(). Можно вызывать напрямую, но принято через встроенные функции - они делают дополнительные проверки типов. У next() есть второй аргумент - дефолтное значение вместо StopIteration:

it = iter([1, 2])
print(next(it, "нет элемента"))  # 1
print(next(it, "нет элемента"))  # 2
print(next(it, "нет элемента"))  # нет элемента  <- не падает с исключением

Генераторы - это итераторы. Функция с yield возвращает генератор, который автоматически реализует протокол итератора - __iter__ и __next__ уже есть. Писать класс с двумя методами вручную почти никогда не нужно - для этого есть генераторы. Но понимать протокол итератора важно, потому что генераторы работают именно по нему.

Бесконечные итераторы. Итератор не обязан когда-либо бросать StopIteration. Можно написать итератор, который генерирует числа до бесконечности. В стандартной библиотеке такие есть - itertools.count(), itertools.cycle(). Работать с ними нужно через break или itertools.islice(), иначе цикл не остановится.

import itertools

# Бесконечный счётчик
counter = itertools.count(start=1, step=2)  # 1, 3, 5, 7, ...
for n in itertools.islice(counter, 5):      # берём только первые 5
    print(n)
# 1, 3, 5, 7, 9

FAQ

В чём разница между итератором и итерируемым объектом? Итерируемый объект - у него есть `__iter__()`, возвращающий итератор. Список, строка, словарь - итерируемые. Итератор - у него есть и `__iter__()`, и `__next__()`. Итератор всегда итерируемый, но не наоборот. Список - итерируемый, но не итератор. `iter(список)` - уже итератор.
Почему итератор нельзя перемотать назад? Потому что итератор по определению хранит только текущую позицию и умеет двигаться только вперёд. Это намеренное ограничение - оно позволяет работать с бесконечными последовательностями и данными, которые физически нельзя перемотать, например сетевой поток. Если нужно пройтись несколько раз - создай новый итератор или преобразуй данные в список.
Чем итератор отличается от генератора? Генератор - это удобный способ создать итератор. Функция с `yield` автоматически реализует протокол итератора, не нужно писать класс с `__iter__` и `__next__` вручную. Любой генератор является итератором, но не любой итератор является генератором - итератор можно написать как класс без `yield`.
Что такое ленивые вычисления и зачем они нужны? Lazy evaluation - вычислять элементы только тогда, когда они нужны, а не все сразу. Итератор не держит все данные в памяти - он отдаёт по одному элементу по запросу. Это критично при работе с большими файлами, базами данных, сетевыми потоками или бесконечными последовательностями - когда загрузить всё в память невозможно или нецелесообразно.
Зачем __iter__ у итератора возвращает сам себя? Чтобы итератор можно было использовать везде, где ожидается итерируемый объект - в цикле `for`, в `zip()`, в `list()` и так далее. Эти конструкции вызывают `iter()` на переданном объекте. Если итератор возвращает сам себя из `__iter__` - он корректно отвечает на этот вызов и продолжает с текущей позиции, а не сбрасывается.
Какие встроенные объекты являются итераторами? Файловые объекты (`open()`), результаты `map()`, `filter()`, `zip()`, `enumerate()`, `reversed()` - всё это итераторы. `range()`, списки, строки, словари, множества - итерируемые объекты, но не итераторы. Проверить просто: `hasattr(obj, '__next__')` - если `True`, это итератор.