$ sudo teach IT
МОДУЛЬ 8 · УРОК 1

Протокол итератора: __iter__ и __next__

Цикл for — одна из первых вещей, которые вы выучили. Но задумывались ли вы, что происходит внутри, когда Python перебирает список или строку? В этом уроке мы вскроем механизм итерации и поймём, почему можно написать for x in любой_объект — если знать правила.

⏱ ~40 минут 🎓 Для новичков 🐍 Python 3

🔑 Два ключевых понятия: Iterable и Iterator

В Python существуют два разных понятия, которые часто путают. Давайте разберём их на простой аналогии, а потом дадим точное определение.

📚 Аналогия: книга и закладка

Представьте книгу — она хранит все страницы. Это Iterable: объект, в котором есть данные для перебора.
Теперь представьте закладку в книге — она знает, на какой странице вы остановились, и умеет переходить к следующей. Это Iterator: объект, который помнит текущую позицию и умеет двигаться вперёд.

📦

Iterable (итерируемый)

Объект, из которого можно получить итератор. У него есть метод __iter__, который возвращает итератор.

Примеры: list, tuple, str, dict, set, range

🔖

Iterator (итератор)

Объект, который умеет отдавать элементы по одному. У него есть метод __next__, возвращающий следующий элемент.

Важно: помнит, где остановился

💡 Точные определения: Iterable — это объект с методом __iter__, который возвращает итератор. Iterator — это объект с методом __next__, который возвращает следующий элемент или бросает StopIteration. Итератор также обязан иметь метод __iter__ (возвращающий самого себя) — это делает его тоже итерируемым.

🛑 Исключение StopIteration — сигнал «всё, элементов больше нет»

Представьте: вы листаете книгу и дошли до последней страницы. Что дальше? Страниц больше нет. В мире Python итератор в этот момент обязан поднять исключение StopIteration.

StopIteration — это не ошибка в привычном смысле. Это сигнал: «я закончил, элементов больше нет». Цикл for внутри себя ловит это исключение и спокойно завершается.

⚠️ Запомните правило: когда метод __next__ понимает, что элементов больше нет — он должен выполнить raise StopIteration. Это договор между итератором и циклом for.

Вот демонстрация того, как StopIteration появляется вручную:

# Создаём итератор из обычного списка
numbers = [10, 20, 30]
it = iter(numbers)      # получаем итератор (разберём это сразу ниже)

print(next(it))         # 10
print(next(it))         # 20
print(next(it))         # 30

# Элементы кончились! Следующий вызов бросает StopIteration:
print(next(it))
# Traceback (most recent call last):
#   ...
# StopIteration

Именно это исключение цикл for ловит автоматически и завершает итерацию. Когда пишете for x in numbers — Python не «падает», увидев StopIteration, а просто выходит из цикла.

🔬 Что происходит под капотом цикла for

Когда вы пишете такой цикл:

fruits = ["яблоко", "банан", "вишня"]

for fruit in fruits:
    print(fruit)

Python выполняет три чётких шага. Давайте разберём каждый:

Шаг 1 — получить итератор

Python вызывает iter(fruits), что внутри делает fruits.__iter__(). Список возвращает специальный объект — list_iterator. Этот объект знает, с какого элемента начать.

Шаг 2 — повторять: получить следующий элемент

Python снова и снова вызывает next(iterator), что внутри делает iterator.__next__(). Каждый раз возвращается следующий элемент и выполняется тело цикла.

Шаг 3 — поймать StopIteration и выйти

Когда элементы заканчиваются, итератор бросает StopIteration. Python его ловит и выходит из цикла. Никакой ошибки — это штатное завершение.

Теперь напишем то же самое вручную, без цикла for — это покажет, что for делает за кулисами:

fruits = ["яблоко", "банан", "вишня"]

# Шаг 1: Python вызывает iter() — получаем итератор
iterator = iter(fruits)          # fruits.__iter__()

# Шаг 2: Python в цикле вызывает next()
fruit = next(iterator)           # iterator.__next__() → "яблоко"
print(fruit)                     # яблоко

fruit = next(iterator)           # iterator.__next__() → "банан"
print(fruit)                     # банан

fruit = next(iterator)           # iterator.__next__() → "вишня"
print(fruit)                     # вишня

# Шаг 3: элементы кончились → StopIteration
# Цикл for поймал бы его автоматически.
# Мы ловим вручную:
try:
    fruit = next(iterator)
except StopIteration:
    print("Элементы закончились!")  # Элементы закончились!

🎯 Вывод: цикл for x in obj — это просто удобная запись для: вызови iter(obj), затем в цикле вызывай next(), пока не поймаешь StopIteration. Python делает всё это за вас автоматически.

🛠️ Встроенные функции iter() и next()

Python предоставляет две встроенные функции, которые напрямую работают с протоколом итератора:

iter(obj)

Вызывает obj.__iter__() и возвращает итератор. Передайте любой итерируемый объект — получите итератор.

Если у объекта нет __iter__ — получите TypeError.

next(iterator)

Вызывает iterator.__next__() и возвращает следующий элемент. Когда элементов нет — бросает StopIteration.

Можно передать второй аргумент: next(it, "default") — вернётся вместо StopIteration.

# iter() и next() в действии

numbers = [1, 2, 3]
it = iter(numbers)

print(type(it))         # <class 'list_iterator'>

print(next(it))         # 1
print(next(it))         # 2
print(next(it))         # 3

# Второй аргумент next() — значение по умолчанию вместо StopIteration
result = next(it, "больше нет")
print(result)           # больше нет

📋 Стандартные итерируемые объекты Python

Все встроенные коллекции Python — итерируемые. Что их объединяет? У каждого из них реализован метод __iter__. Именно поэтому вы можете писать for x in ... с любым из них:

📝

list

[1, 2, 3]
Итерирует элементы по порядку

🔒

tuple

(1, 2, 3)
Как list, но неизменяемый

📖

str

"hello"
Итерирует посимвольно

🗂️

dict

{"a": 1}
Итерирует ключи

🔢

range

range(5)
Числа без хранения всех сразу

🔵

set

{1, 2, 3}
Итерирует без гарантии порядка

# Все эти объекты — Iterable: к ним применим iter() и for

# Строка
for char in "abc":
    print(char)         # a, b, c

# Словарь — итерирует ключи
data = {"x": 1, "y": 2}
for key in data:
    print(key)          # x, y

# range — не хранит все числа в памяти, генерирует на ходу
for i in range(3):
    print(i)            # 0, 1, 2

⚠️ Важно: list — Iterable, но не Iterator!

Это типичная точка путаницы для новичков. Разберём её подробно.

У списка есть __iter__ — значит, он Iterable. Но у него нет метода __next__ — значит, он не Iterator.

numbers = [1, 2, 3]

# Список — Iterable: у него есть __iter__
print(hasattr(numbers, '__iter__'))     # True

# Список — НЕ Iterator: у него нет __next__
print(hasattr(numbers, '__next__'))     # False

# Поэтому вызвать next() прямо на списке НЕЛЬЗЯ:
# next(numbers)   →   TypeError: 'list' object is not an iterator

# Нужно сначала получить итератор через iter():
it = iter(numbers)                      # Вот теперь это Iterator

print(hasattr(it, '__iter__'))          # True
print(hasattr(it, '__next__'))          # True  ← теперь есть __next__

print(next(it))                         # 1

🔄 Каждый for создаёт новый итератор. Именно поэтому вы можете запустить цикл for по одному списку несколько раз — каждый раз Python вызывает iter() заново и получает свежий итератор, начинающий с нуля. Итераторы же одноразовые: пройдя до конца, они не возвращаются назад.

numbers = [1, 2, 3]

# Список можно итерировать сколько угодно раз — каждый for создаёт новый итератор
for x in numbers:
    print(x)            # 1, 2, 3

for x in numbers:
    print(x)            # 1, 2, 3  — снова с начала!

# А вот итератор — одноразовый:
it = iter(numbers)

for x in it:
    print(x)            # 1, 2, 3

for x in it:
    print(x)            # — ничего! Итератор исчерпан.

🔢 range — умный Iterable

range — хороший пример для понимания разницы между Iterable и Iterator. Сам range — это Iterable: он описывает диапазон чисел, но не хранит их все в памяти. Когда вы вызываете iter(range(...)), Python создаёт специальный range_iterator — вот он уже Iterator.

r = range(3)

print(type(r))              # <class 'range'>      — это Iterable

it = iter(r)
print(type(it))             # <class 'range_iterator'>  — это Iterator

print(next(it))             # 0
print(next(it))             # 1
print(next(it))             # 2
# next(it) → StopIteration

# range — тоже "многоразовый", как list:
for i in r:
    print(i)                # 0, 1, 2

for i in r:
    print(i)                # 0, 1, 2  — снова с начала

# Почему range экономит память?
# range(1_000_000) НЕ создаёт список из миллиона чисел.
# Он хранит только start, stop, step — три числа.
# А iterator вычисляет каждое следующее число по требованию.
import sys
print(sys.getsizeof(range(1_000_000)))   # 48 байт — всегда!
print(sys.getsizeof(list(range(1_000_000))))  # ~8 000 056 байт

🌱 Этот принцип «вычисляй по требованию» называется ленивыми вычислениями (lazy evaluation). Итератор не знает заранее все элементы — он вычисляет каждый следующий только в момент вызова next(). Это экономит память и позволяет работать с бесконечными последовательностями. О ленивых вычислениях мы поговорим подробнее, когда будем изучать генераторы.

🗺️ Полная картина: как всё связано

Давайте соберём всё, что узнали, в единую схему:

Объект Iterable? Iterator? Что возвращает iter()
list ✅ да ❌ нет list_iterator
tuple ✅ да ❌ нет tuple_iterator
str ✅ да ❌ нет str_ascii_iterator
dict ✅ да ❌ нет dict_keyiterator
set ✅ да ❌ нет set_iterator
range ✅ да ❌ нет range_iterator
list_iterator и др. ✅ да ✅ да самого себя (self)

Обратите внимание на последнюю строку: сам итератор тоже является итерируемым. Когда вы вызываете iter() на итераторе — он возвращает self. Это позволяет использовать итераторы везде, где ожидается итерируемый объект.

✅ Итоги урока

  • Iterable — объект с методом __iter__, который умеет создать итератор. Примеры: list, tuple, str, dict, set, range.
  • Iterator — объект с методом __next__, который умеет отдавать элементы по одному. Итератор также имеет __iter__, возвращающий самого себя.
  • StopIteration — исключение-сигнал «элементы закончились». Цикл for ловит его и завершается без ошибки.
  • Цикл for x in obj внутри: вызывает iter(obj), затем в цикле вызывает next(), ловит StopIteration.
  • list — Iterable, но не Iterator: у него нет __next__. Каждый for создаёт новый итератор через iter().
  • range — Iterable, iter(range(...)) возвращает range_iterator. Сам range не хранит числа в памяти — только start, stop, step.

👉 В следующем уроке мы напишем собственный итератор с нуля — реализуем методы __iter__ и __next__ в своём классе. Вы увидите, как создать объект, который можно использовать в цикле for так же, как список или range.