35. Как работает итерация: итерируемые объекты и итераторы
На протяжении этой книги вы использовали циклы for, чтобы перебирать (выполнять итерацию(iteration) по) спискам, строкам, словарям и другим коллекциям. Вы бесчисленное количество раз писали код вроде for item in my_list:. Но что на самом деле происходит «за кулисами», когда Python выполняет цикл for? Откуда Python знает, как шаг за шагом проходить по разным типам коллекций?
В этой главе мы исследуем протокол итерации(iteration protocol) Python — механизм, который заставляет работать циклы for. Вы узнаете об итерируемых объектах(iterables) (объектах, по которым можно выполнять цикл) и итераторах(iterators) (объектах, которые фактически выполняют пошаговый проход по значениям). Понимание этого различия углубит ваше представление о том, как работает Python, и подготовит вас к работе с генераторами в главе 36.
35.1) Что значит, что объект является итерируемым
35.1.1) Понятие итерируемости
Итерируемый объект(iterable) — это любой объект Python, по которому можно пройти в цикле for. Когда мы говорим «пройти в цикле», мы имеем в виду, что Python может получать элементы объекта по одному, последовательно.
Вы уже работали со многими итерируемыми объектами:
# Списки являются итерируемыми
numbers = [1, 2, 3, 4, 5]
for num in numbers:
print(num) # Output: 1, 2, 3, 4, 5 (on separate lines)
# Строки являются итерируемыми
text = "Python"
for char in text:
print(char) # Output: P, y, t, h, o, n (on separate lines)
# Словари являются итерируемыми (по умолчанию — по ключам)
student = {"name": "Alice", "age": 20, "grade": "A"}
for key in student:
print(key) # Output: name, age, grade (on separate lines)Все эти объекты — списки, строки, словари, кортежи, множества, диапазоны и файлы — являются итерируемыми объектами, потому что они поддерживают протокол итерации(iteration protocol) Python (набор правил, который позволяет Python выполнять по ним цикл).
35.1.2) Что делает объект итерируемым
Чтобы объект был итерируемым, он должен реализовывать специальный метод __iter__(). Этот метод возвращает объект-итератор(iterator). Пока не беспокойтесь о деталях — итераторы мы рассмотрим в следующем разделе.
Вы можете проверить, является ли объект итерируемым, попробовав получить из него итератор с помощью встроенной функции iter():
# Проверка, являются ли объекты итерируемыми
numbers = [1, 2, 3]
iterator = iter(numbers) # Работает — списки итерируемы
print(type(iterator)) # Output: <class 'list_iterator'>
text = "Hello"
iterator = iter(text) # Работает — строки итерируемы
print(type(iterator)) # Output: <class 'str_iterator'>
# Попытка с неитерируемым объектом
value = 42
try:
iterator = iter(value) # Не удаётся — целые числа не итерируемы
except TypeError as e:
print(f"Error: {e}") # Output: Error: 'int' object is not iterableКогда вы вызываете iter() для итерируемого объекта, Python вызывает метод __iter__() этого объекта и возвращает итератор. Если у объекта нет этого метода, вы получаете TypeError.
35.1.3) Итерируемые объекты vs последовательности
Важно понимать, что не все итерируемые объекты являются последовательностями. Последовательность — это специфический тип итерируемого объекта, который поддерживает индексацию и имеет определённый порядок.
# Последовательности поддерживают индексацию
my_list = [10, 20, 30]
print(my_list[0]) # Output: 10
my_string = "Python"
print(my_string[2]) # Output: t
# Множества итерируемы, но НЕ являются последовательностями (нет индексации, нет гарантированного порядка)
my_set = {1, 2, 3}
for item in my_set:
print(item) # Работает — множества итерируемы
# Но индексация не работает
try:
print(my_set[0]) # Не удаётся — множества не поддерживают индексацию
except TypeError as e:
print(f"Error: {e}") # Output: Error: 'set' object is not subscriptableКлючевое различие: Все последовательности (списки, кортежи, строки, диапазоны) являются итерируемыми объектами, но не все итерируемые объекты являются последовательностями. Множества и словари — итерируемые объекты, но не последовательности, потому что они не поддерживают индексацию.
35.1.4) Почему итерируемость важна
Понимание итерируемости помогает вам:
- Знать, по чему можно выполнять цикл: любой итерируемый объект работает с циклами
for - Понимать сообщения об ошибках: "object is not iterable" означает, что его нельзя использовать в цикле
for - Использовать включения(comprehensions): списков, множеств и словарей работают с любым итерируемым объектом
- Работать со встроенными функциями: многие встроенные функции, такие как
sum(),max(),min()иsorted(), принимают любой итерируемый объект
# Всё это работает, потому что принимает итерируемые объекты
numbers = [1, 2, 3, 4, 5]
print(sum(numbers)) # Output: 15
text = "Python"
print(max(text)) # Output: y (highest alphabetically)
# Работает даже с множествами
unique_values = {10, 5, 20, 15}
print(sorted(unique_values)) # Output: [5, 10, 15, 20]35.2) Повседневные итераторы в Python (файлы, диапазоны, словари и не только)
35.2.1) Что такое итератор
Итератор(iterator) — это объект, представляющий поток данных. Он возвращает по одному значению за раз, когда вы запрашиваете следующий элемент. Как только итератор вернул все свои значения, он исчерпан и не может быть использован повторно.
Думайте об итераторе как о закладке в книге:
- Он помнит, где вы находитесь в последовательности
- Вы можете запросить следующий элемент
- Как только вы достигли конца, вы не можете вернуться назад без создания нового итератора
Ключевое различие между итерируемым объектом и итератором:
- Итерируемый объект(iterable) — это то, по чему можно выполнять итерацию (например, список)
- Итератор(iterator) — это объект, который выполняет итерацию (механизм, который проходит по списку)
# Список — это итерируемый объект
numbers = [1, 2, 3]
# Получаем итератор из итерируемого объекта
iterator = iter(numbers)
# Итератор — это отдельный объект
print(type(numbers)) # Output: <class 'list'>
print(type(iterator)) # Output: <class 'list_iterator'>35.2.2) Итераторы в циклах for
Когда вы пишете цикл for, Python автоматически создаёт итератор «за кулисами»:
numbers = [10, 20, 30]
# Что вы пишете:
for num in numbers:
print(num)
# Что Python делает внутри (концептуально):
# 1. Вызвать iter(numbers), чтобы получить итератор
# 2. Многократно вызывать next() для итератора
# 3. Остановиться, когда итератор выбросит StopIterationВот как это выглядит явно:
numbers = [10, 20, 30]
# Ручная итерация (то, что for делает автоматически)
iterator = iter(numbers)
try:
print(next(iterator)) # Output: 10
print(next(iterator)) # Output: 20
print(next(iterator)) # Output: 30
print(next(iterator)) # Would raise StopIteration
except StopIteration:
print("No more items") # Output: No more itemsЦикл for обрабатывает исключение StopIteration автоматически, поэтому вы никогда не видите его в обычном коде.
35.2.3) Файловые объекты как итераторы
Файловые объекты — отличные примеры итераторов. Когда вы выполняете итерацию по файлу, он читает по одной строке за раз:
# Создаём пример файла
with open("students.txt", "w") as file:
file.write("Alice\n")
file.write("Bob\n")
file.write("Charlie\n")
# Читаем файл построчно
with open("students.txt", "r") as file:
for line in file:
print(line.strip()) # Output: Alice, Bob, Charlie (on separate lines)Файловые объекты одновременно и итерируемые, и итераторы. Они возвращают сами себя, когда вы вызываете для них iter():
with open("students.txt", "r") as file:
iterator = iter(file)
print(file is iterator) # Output: True (same object)
# Читаем строки вручную
print(next(iterator)) # Output: Alice
print(next(iterator)) # Output: Bob
print(next(iterator)) # Output: CharlieЭто эффективно по памяти, потому что Python не загружает весь файл в память — он читает по одной строке за раз по вашему запросу.
35.2.4) Объекты range как итераторы
Объекты range — это итерируемые объекты, которые генерируют числа по требованию:
# range — это итерируемый объект
numbers = range(1, 4)
print(type(numbers)) # Output: <class 'range'>
# Получаем итератор из range
iterator = iter(numbers)
print(type(iterator)) # Output: <class 'range_iterator'>
# Используем итератор
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3Диапазоны экономичны по памяти, потому что они не хранят все числа в памяти — они вычисляют каждое число по запросу:
# Этот range представляет 1 миллион чисел, но использует минимум памяти
large_range = range(1000000)
print(type(large_range)) # Output: <class 'range'>
# Получаем итератор
iterator = iter(large_range)
print(next(iterator)) # Output: 0
print(next(iterator)) # Output: 1
# ... можно продолжать для 1 миллиона значений35.2.5) Итераторы словарей
Словари предоставляют разные итераторы для ключей, значений и элементов:
student = {"name": "Alice", "age": 20, "grade": "A"}
# Итерация по ключам (по умолчанию)
for key in student:
print(key) # Output: name, age, grade (on separate lines)
# Явно получаем итератор ключей
keys_iterator = iter(student.keys())
print(next(keys_iterator)) # Output: name
print(next(keys_iterator)) # Output: age
# Итерация по значениям
values_iterator = iter(student.values())
print(next(values_iterator)) # Output: Alice
print(next(values_iterator)) # Output: 20
# Итерация по элементам (пары ключ-значение)
items_iterator = iter(student.items())
print(next(items_iterator)) # Output: ('name', 'Alice')
print(next(items_iterator)) # Output: ('age', 20)35.2.6) Итераторы исчерпаемы
Важное свойство итераторов заключается в том, что их можно использовать только один раз. После исчерпания они не «сбрасываются»:
numbers = [1, 2, 3]
iterator = iter(numbers)
# Первый проход по итератору
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3
# Теперь итератор исчерпан
try:
print(next(iterator)) # Raises StopIteration
except StopIteration:
print("Iterator exhausted") # Output: Iterator exhausted
# Чтобы пройти снова, создайте новый итератор
iterator = iter(numbers)
print(next(iterator)) # Output: 1 (fresh start)Это отличается от самого итерируемого объекта, по которому можно выполнять итерацию несколько раз:
numbers = [1, 2, 3]
# Первая итерация
for num in numbers:
print(num) # Output: 1, 2, 3
# Вторая итерация (работает нормально — создаётся новый итератор)
for num in numbers:
print(num) # Output: 1, 2, 335.3) Использование iter() и next() для пошагового прохода по итерируемым объектам
35.3.1) Функция iter()
Функция iter() принимает итерируемый объект и возвращает итератор. Это первый шаг в протоколе итерации:
# Создаём итераторы из разных итерируемых объектов
numbers = [10, 20, 30]
iterator = iter(numbers)
print(type(iterator)) # Output: <class 'list_iterator'>
text = "Hi"
text_iterator = iter(text)
print(type(text_iterator)) # Output: <class 'str_iterator'>
my_set = {1, 2, 3}
set_iterator = iter(my_set)
print(type(set_iterator)) # Output: <class 'set_iterator'>Каждый тип итерируемого объекта возвращает свой специализированный тип итератора, но все они работают одинаково — вы вызываете next(), чтобы получить следующее значение.
35.3.2) Функция next()
Функция next() извлекает следующий элемент из итератора. Когда элементов больше нет, она выбрасывает StopIteration:
colors = ["red", "green", "blue"]
iterator = iter(colors)
# Получаем элементы по одному
print(next(iterator)) # Output: red
print(next(iterator)) # Output: green
print(next(iterator)) # Output: blue
# Элементов больше нет
try:
print(next(iterator)) # Raises StopIteration
except StopIteration:
print("No more colors") # Output: No more colors35.3.3) Передача значения по умолчанию в next()
Вы можете передать значение по умолчанию во втором аргументе next(). Когда итератор исчерпан, вместо выбрасывания исключения StopIteration функция next() вернёт указанное вами значение по умолчанию:
numbers = [1, 2, 3]
iterator = iter(numbers)
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3
print(next(iterator, "Done")) # Output: Done (default value, no exception)
print(next(iterator, "Done")) # Output: Done (still exhausted)Это полезно, когда вы хотите корректно обработать конец итерации без обработки исключений:
35.4) Создание пользовательских итераторов с iter и next
35.4.1) Зачем создавать пользовательские итераторы
Встроенные итерируемые объекты Python (списки, строки, файлы) покрывают большинство распространённых случаев. Однако иногда вам нужно создавать собственные итерируемые объекты для специализированного поведения:
- Генерация последовательностей с пользовательской логикой
- Итерация по структурам данных, которые вы проектируете
- Создание экономичной по памяти итерации по большим наборам данных
- Реализация ленивых вычислений(lazy evaluation) (вычисление значений только по мере необходимости)
Создание пользовательского итератора требует реализации двух специальных методов: __iter__() и __next__().
35.4.2) Протокол итератора
Чтобы сделать объект итератором, он должен реализовывать:
__iter__(): Возвращает сам объект-итератор (обычноself)__next__(): Возвращает следующее значение в последовательности или выбрасываетStopIteration, когда всё завершено
class SimpleCounter:
"""Итератор, который считает от start до end."""
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
"""Вернуть объект-итератор (self)."""
return self
def __next__(self):
"""Вернуть следующее значение или выбросить StopIteration."""
if self.current > self.end:
raise StopIteration
value = self.current
self.current += 1
return value
# Используем пользовательский итератор
counter = SimpleCounter(1, 5)
for num in counter:
print(num)
# Output: 1
# Output: 2
# Output: 3
# Output: 4
# Output: 5Давайте разберём, что происходит:
- Цикл
forвызываетiter(counter), что вызываетcounter.__iter__()и получает обратно самcounter - Цикл многократно вызывает
next(counter), что вызываетcounter.__next__() - Каждый вызов
__next__()возвращает следующее число и увеличиваетcurrent - Когда
current > end,__next__()выбрасываетStopIteration, и цикл останавливается
35.4.3) Ручное использование пользовательских итераторов
Вы также можете использовать пользовательские итераторы вручную с iter() и next():
counter = SimpleCounter(10, 13)
# Получаем итератор (возвращает самого себя)
iterator = iter(counter)
print(iterator is counter) # Output: True
# Получаем значения вручную
print(next(iterator)) # Output: 10
print(next(iterator)) # Output: 11
print(next(iterator)) # Output: 12
print(next(iterator)) # Output: 13
# Теперь исчерпан
try:
print(next(iterator))
except StopIteration:
print("Counter exhausted") # Output: Counter exhausted35.4.4) Итераторы исчерпаемы (снова)
Помните, что итераторы можно использовать только один раз:
counter = SimpleCounter(1, 3)
# Первая итерация
for num in counter:
print(num) # Output: 1, 2, 3
# Вторая итерация (не работает — итератор исчерпан)
for num in counter:
print(num) # Nothing printed - iterator is already exhaustedЧтобы выполнить итерацию снова, нужно создать новый экземпляр:
# Создавайте новый counter для каждой итерации
for num in SimpleCounter(1, 3):
print(num) # Output: 1, 2, 3
for num in SimpleCounter(1, 3):
print(num) # Output: 1, 2, 3 (new iterator)35.4.5) Создание итерируемого класса (а не только итератора)
Часто вам нужен класс, который является итерируемым, но каждый раз создаёт новый «свежий» итератор. Для этого отделите итерируемый объект от итератора:
class CounterIterable:
"""Итерируемый объект, который создаёт новые итераторы счётчика."""
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
"""Каждый раз возвращать новый итератор."""
return CounterIterator(self.start, self.end)
class CounterIterator:
"""Фактический итератор, который выполняет подсчёт."""
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
if self.current > self.end:
raise StopIteration
value = self.current
self.current += 1
return value
# Теперь можно выполнять итерацию несколько раз
counter = CounterIterable(1, 3)
# Первая итерация
for num in counter:
print(num) # Output: 1, 2, 3
# Вторая итерация (работает, потому что __iter__ создаёт новый итератор)
for num in counter:
print(num) # Output: 1, 2, 3Этот шаблон разделяет ответственность:
CounterIterable— итерируемый объект: он знает, как создавать итераторыCounterIterator— итератор: он знает, как шагать по значениям
35.4.6) Практический пример: итерация по пользовательской структуре данных
Давайте создадим итератор для пользовательской структуры данных — простого плейлиста:
class Playlist:
"""Музыкальный плейлист, по которому можно выполнять итерацию."""
def __init__(self):
self.songs = []
def add_song(self, title, artist):
"""Добавить песню в плейлист."""
self.songs.append({"title": title, "artist": artist})
def __iter__(self):
"""Вернуть итератор для плейлиста."""
return PlaylistIterator(self.songs)
class PlaylistIterator:
"""Итератор для пошагового прохода по песням в плейлисте."""
def __init__(self, songs):
self.songs = songs
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index >= len(self.songs):
raise StopIteration
song = self.songs[self.index]
self.index += 1
return song
# Используем плейлист
playlist = Playlist()
playlist.add_song("Imagine", "John Lennon")
playlist.add_song("Bohemian Rhapsody", "Queen")
playlist.add_song("Hotel California", "Eagles")
# Проходим по песням
print("Now playing:")
for song in playlist:
print(f" {song['title']} by {song['artist']}")
# Output: Now playing:
# Output: Imagine by John Lennon
# Output: Bohemian Rhapsody by Queen
# Output: Hotel California by Eagles
# Можно пройти снова (создаётся новый итератор)
print("\nReplay:")
for song in playlist:
print(f" {song['title']}")
# Output: Replay:
# Output: Imagine
# Output: Bohemian Rhapsody
# Output: Hotel California35.4.7) Когда использовать пользовательские итераторы
Создавайте пользовательские итераторы, когда:
- Вам нужна ленивая оценка(lazy evaluation): генерировать значения по требованию, а не хранить их все
- У вас есть пользовательская структура данных: сделать её итерируемой, чтобы она работала с циклами
for - Вам нужна особая логика итерации: пропускать элементы, преобразовывать значения или реализовать сложный шаг
- Важна эффективность по памяти: генерировать большие последовательности без хранения их целиком
Однако, в главе 36 вы узнаете о генераторах(generators), которые предоставляют гораздо более простой способ создавать итераторы с использованием ключевого слова yield. Генераторы обычно предпочтительнее ручной реализации __iter__() и __next__(), потому что они более лаконичны и их проще понимать.
Понимание того, как создавать пользовательские итераторы, даёт вам представление о том, как работает протокол итерации Python, даже если чаще вы будете использовать вместо этого генераторы. Концепции, которые вы изучили здесь — __iter__(), __next__() и StopIteration — фундаментальны для понимания генераторов и других продвинутых техник итерации в следующей главе.