Протокол итератора
Что делает 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
Напишите класс-итератор 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) == [], "Итератор одноразовый: после исчерпания он пуст"
class Countdown:
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
Сделайте класс 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 — итерируемый объект, а не итератор"
class Sentence:
def __init__(self, text):
self.text = text
def __iter__(self):
return iter(self.text.split())
Что выведет код?
nums = map(str, [1, 2, 3])
print(list(nums))
print(list(nums))
Итерируемый объект или уже итератор?
| итерируемый, но не итератор | итератор | |
|---|---|---|
[1, 2, 3] |
||
range(10) |
||
iter([1, 2]) |
||
zip(a, b) |
||
{"a": 1} |
||
открытый файл open(...) |
||
(x for x in a) |
Чем итерируемый объект (iterable) отличается от итератора (iterator)?
Войдите, чтобы отмечать пройденные уроки.