Итераторы в Python

Каждый раз, когда вы пишете for x in something:, под капотом работает итератор. Понимание этого механизма — ключ к глубокому пониманию Python: на итераторах построены циклы, генераторы, map, filter, разбор файлов построчно, библиотеки вроде itertools и даже большая часть стандартной библиотеки.


Что такое итератор

Итератор — это объект, который умеет выдавать значения по одному, «запоминая» текущую позицию между вызовами. Формально это любой объект, у которого есть метод __next__(), возвращающий следующий элемент или возбуждающий StopIteration, когда элементов больше нет.

Итерируемый объект (iterable) — это объект, из которого можно получить итератор: список, строка, кортеж, словарь, множество, файл. У него есть метод __iter__(), возвращающий новый итератор.

[список]  --iter()-->  [итератор]  --next()-->  элемент 1
                                    --next()-->  элемент 2
                                    --next()-->  элемент 3
                                    --next()-->  StopIteration

Важное различие: список — итерируемый, но не итератор. От одного списка можно получить сколько угодно независимых итераторов.


Протокол итерации на пальцах

Что делает Python, когда вы пишете for x in [10, 20, 30]::

  1. Вызывает iter([10, 20, 30]) — получает объект-итератор.

  2. В цикле вызывает next(итератор) — получает очередное значение, кладёт его в x.

  3. Повторяет шаг 2, пока next не возбудит StopIteration.

  4. Цикл завершается.

Эквивалентный код вручную:

numbers = [10, 20, 30]
it = iter(numbers)
while True:
    try:
        x = next(it)
    except StopIteration:
        break
    print(x)

Вывод:

10
20
30

Функции iter() и next()

  • iter(obj) — получить итератор из итерируемого объекта.

  • next(it) — получить следующий элемент. С двумя аргументами next(it, default) вместо StopIteration вернёт default.

it = iter('Привет')
print(next(it))   # П
print(next(it))   # р
print(next(it))   # и

Итератор по строке файла

Файл в Python — сам по себе итератор. Каждый next() возвращает следующую строку. Поэтому работает идиома:

with open('log.txt', encoding='utf-8') as f:
    for line in f:
        print(line.rstrip())

Файл читается строка за строкой и не грузит весь объём в память — это важно для больших логов.


Свой класс-итератор

Чтобы объект стал итератором, нужно реализовать два метода:

  • __iter__(self) — возвращает сам объект (return self);

  • __next__(self) — возвращает следующее значение или возбуждает StopIteration.

Классический пример — счётчик от 1 до n:

class Counter:
    def __init__(self, n):
        self.n = n
        self.i = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.i >= self.n:
            raise StopIteration
        self.i += 1
        return self.i

for x in Counter(3):
    print(x)

Вывод:

1
2
3

Практический пример: пагинация API

Итераторы отлично подходят для постраничной загрузки данных — каждая страница запрашивается лениво, только когда нужна.

class PagedAPI:
    def __init__(self, url):
        self.url = url
        self.page = 1
        self.buffer = []

    def __iter__(self):
        return self

    def __next__(self):
        if not self.buffer:
            self.buffer = self._fetch(self.page)
            self.page += 1
            if not self.buffer:
                raise StopIteration
        return self.buffer.pop(0)

    def _fetch(self, page):
        # здесь обращение к реальному API
        return [{'id': page * 10 + i} for i in range(3)] if page <= 2 else []

for item in PagedAPI('https://example.com/items'):
    print(item)

Практический пример: ленивая бесконечная последовательность

Итератор может быть бесконечным — это нормально, главное вовремя остановиться.

from itertools import islice

class Fibonacci:
    def __iter__(self):
        a, b = 0, 1
        while True:
            yield a              # это уже генератор — частный случай итератора
            a, b = b, a + b

print(list(islice(Fibonacci(), 10)))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

islice — отрезает первые n элементов от любого итератора.


Итерируемый vs итератор

Сравнительная таблица:

                       iterable      iterator
__iter__()             есть          есть (возвращает self)
__next__()             нет           есть
for x in ...           да            да (но один раз!)
iter()                 даёт новый    даёт сам себя

Главная практическая разница: итератор одноразовый. Если вы прошли по нему, второй раз ничего не получите. Список можно перебирать сколько угодно раз.

nums = [1, 2, 3]
it = iter(nums)
print(list(it))   # [1, 2, 3]
print(list(it))   # []   <- итератор исчерпан

Подводные камни

Предупреждение

  • Итератор одноразовый. После полного обхода — пустой.

  • Никогда не возвращайте новый итератор из ``__next__``. Возвращайте элемент. __iter__ возвращает итератор (часто self).

  • Не забывайте про ``StopIteration``. Без него цикл будет вечным.

  • При параллельной итерации создавайте отдельные итераторы. Иначе они будут «съедать» одни и те же значения.


Смотрите также


Примечание

Лицензия и источники

Часть материала адаптирована из официальной документации Python (https://docs.python.org/3/tutorial/classes.html#iterators), доступной под Python Software Foundation License Version 2 (PSF License). Адаптация, переработка, оригинальные примеры и пояснения — © AlashEd Wiki.