Итератор в Python
Каждый раз когда ты пишешь for - ты используешь итератор. Пора наконец понять как это работает изнутри
Содержание
Коротко и чётко
Итератор - это объект, который реализует два метода:
__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