Протокол итератора: __iter__ и __next__
Цикл for — одна из первых вещей, которые вы выучили. Но задумывались ли вы, что происходит внутри, когда Python перебирает список или строку? В этом уроке мы вскроем механизм итерации и поймём, почему можно написать for x in любой_объект — если знать правила.
🔑 Два ключевых понятия: 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.