Протокол итератора

⏱ 30 мин · 🧩 заданий: 5

Что делает for на самом деле

Цикл

for x in collection:
    print(x)

Python выполняет примерно так:

collection = ["а", "б", "в"]

it = iter(collection)          # 1. просим у коллекции итератор: collection.__iter__()
while True:
    try:
        x = next(it)           # 2. берём следующий элемент: it.__next__()
    except StopIteration:      # 3. элементы кончились — выходим
        break
    print(x)

Пройдите этот пример 👣 Пошагово — видно, как next сдвигает итератор.

Два протокола

Понятие Что должно быть Пример
Итерируемый объект (iterable) __iter__(), возвращающий итератор list, str, dict, set, range, файл
Итератор (iterator) __next__() + __iter__(), возвращающий self iter(list), map, zip, генератор, файл
nums = [10, 20, 30]
it = iter(nums)
print(type(it).__name__, iter(it) is it)     # у итератора iter() возвращает его самого
print(next(it), next(it), next(it))
try:
    next(it)
except StopIteration:
    print("StopIteration — элементов больше нет")
print(next(iter(nums)))                       # новый итератор начинает сначала
print(next(it, "значение по умолчанию"))      # второй аргумент next — вместо исключения

Итератор одноразовый

Итератор помнит позицию и умеет только идти вперёд. Список можно обойти сколько угодно раз: каждый for берёт новый итератор. А итератор после исчерпания пуст навсегда.

squares = map(lambda x: x * x, [1, 2, 3])
print(list(squares))
print(list(squares))          # уже пусто!
print(3 in map(int, "123"))       # in тоже перебирает итератор

pairs = zip("abc", [1, 2, 3])
for letter, n in pairs:
    if letter == "b":
        break
print(list(pairs))            # продолжает с места остановки

Свой итератор

class Fibonacci:
    """Итератор первых n чисел Фибоначчи."""
    def __init__(self, n):
        self.n = n
        self.a, self.b = 0, 1
        self.count = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.count >= self.n:
            raise StopIteration
        value = self.a
        self.a, self.b = self.b, self.a + self.b
        self.count += 1
        return value

print(list(Fibonacci(10)))
for i, f in enumerate(Fibonacci(5)):
    print(i, f)

Итерируемый контейнер

Если класс сам будет итератором, его можно обойти только один раз. Правильный контейнер возвращает новый итератор при каждом iter():

class Team:
    def __init__(self, *members):
        self.members = list(members)

    def __iter__(self):
        return iter(self.members)        # делегируем итератору списка

    def __len__(self):
        return len(self.members)

team = Team("Анна", "Боря", "Вера")
print(list(team), list(team))            # оба прохода работают
print("Боря" in team, sorted(team, reverse=True), len(team))
a, b, c = team                           # распаковка тоже через итерацию
print(c)

На практике __iter__ чаще всего пишут генератором (yield) — это следующий урок.

Где итераторы повсюду

d = {"a": 1, "b": 2}
print(iter(d), iter(d.items()))         # словарь итерирует ключи
print(enumerate("ab"), reversed([1, 2]), zip())
print(list(enumerate("ab", start=1)))

text = "строка 1\nстрока 2\n"
import io
f = io.StringIO(text)                   # файлоподобный объект — итератор строк
print(next(f), end="")
print(list(f))

Ленивость

Итераторы выдают элементы по одному, по запросу. Поэтому range(10**12) не занимает память, а цепочка map/filter ничего не вычисляет, пока её не начнут перебирать:

def noisy(x):
    print(f"  обрабатываю {x}")
    return x * 10

lazy = map(noisy, range(1_000_000_000))   # мгновенно: ничего не вычислено
print("map создан")
print(next(lazy))
print(next(lazy))

iter(callable, sentinel)

Вторая форма iter вызывает функцию, пока она не вернёт «стоп-значение». Удобно для чтения блоками:

import io
stream = io.BytesIO(b"abcdefghij")
for chunk in iter(lambda: stream.read(4), b""):    # читать по 4 байта до пустого результата
    print(chunk)

Вопрос на собеседовании

«Как работает for в Python?» — for вызывает iter() у объекта, получает итератор и в цикле вызывает next(), пока не будет брошено StopIteration. Любой объект с __iter__ (или старым __getitem__ с индексами от 0) можно перебирать.

Итог

  • for = iter() + next() до StopIteration.
  • Итерируемый объект выдаёт итераторы; итератор хранит позицию и одноразовый.
  • map, zip, enumerate, filter, файлы, генераторы — итераторы; list, dict, range — нет.
  • Контейнер должен возвращать новый итератор при каждом вызове __iter__.
  • Итераторы ленивы: элементы вычисляются по запросу.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Напишите класс-итератор Countdown(start), который выдаёт числа start, start-1, …, 1 и затем завершается. Реализуйте __iter__ и __next__ (без генераторов и yield).

Какие тесты проверяют код (4)
assert list(Countdown(3)) == [3, 2, 1]
assert list(Countdown(0)) == []
it = Countdown(2); assert next(it) == 2 and next(it) == 1
it = Countdown(1); list(it); assert list(it) == [], "Итератор одноразовый: после исчерпания он пуст"
Задание 2 Программа на Python

Сделайте класс Sentence(text) итерируемым (но не итератором): каждый for по нему должен заново перебирать слова текста. Подсказка: __iter__ может вернуть iter(...) от списка слов.

Какие тесты проверяют код (3)
s = Sentence("мама мыла раму"); assert list(s) == ["мама", "мыла", "раму"]
s = Sentence("a b"); assert list(s) == list(s) == ["a", "b"], "Повторный обход должен работать"
assert not hasattr(Sentence("x"), "__next__"), "Sentence — итерируемый объект, а не итератор"
Задание 3 Один вариант ответа

Что выведет код?

nums = map(str, [1, 2, 3])
print(list(nums))
print(list(nums))
Задание 4 Заполнить таблицу

Итерируемый объект или уже итератор?

итерируемый, но не итераторитератор
[1, 2, 3]
range(10)
iter([1, 2])
zip(a, b)
{"a": 1}
открытый файл open(...)
(x for x in a)
Отметьте ячейку — «+», пустая — «−».
Задание 5 Один вариант ответа

Чем итерируемый объект (iterable) отличается от итератора (iterator)?

Войдите, чтобы отмечать пройденные уроки.