Итератор и итерируемый объект
Итерируемый объект (iterable) — это объект, который может состоять из множества элементов и предоставляет их по одному. Примеры: список, кортеж, строка, множество, словарь.
Итератор (iterator) — это объект, который позволяет поочерёдно получать элементы коллекции, не загружая их все в память сразу. Он хранит:
- ссылку на итерируемый объект (источник данных);
- текущую позицию;
- логику получения следующего элемента.
Итератор не изменяет исходную коллекцию, а только управляет последовательным извлечением данных. Итераторы используются в циклах for, ленивых вычислениях, чтении файлов построчно и везде, где важна эффективность.
Магические методы итерации
Итерация в Python основана на dunder-методах (double underscore):
__iter__()— создаёт объект итератора из итерируемого объекта.__next__()— выдаёт следующий элемент итератора.
Хотя напрямую вызывать эти методы можно (numbers.__iter__(), iterator.__next__()), в современном коде предпочитают встроенные функции iter() и next() — они делают то же самое, но читаются понятнее.
Встроенные функции iter() и next()
iter(obj)— вызываетobj.__iter__()и возвращает итератор.next(iterator)— вызываетiterator.__next__()и возвращает следующий элемент.next(iterator, default)— возвращаетdefault, если элементы закончились, вместо исключенияStopIteration.
Исключение StopIteration
Когда итератор исчерпал все элементы, вызов __next__() поднимает StopIteration. Цикл for ловит это исключение автоматически и завершается без ошибки.
Ручной вызов next() без обработки приведёт к остановке программы, если данные закончились. Поэтому в таких случаях удобно передавать второй аргумент-значение по умолчанию.
Как работает цикл for
Цикл for element in iterable: внутри выполняет следующее:
- Вызывает
iter(iterable), чтобы получить итератор. - Вызывает
next(iterator)и передаёт результат вelement. - Повторяет шаг 2, пока
next()не вызоветStopIteration. - При
StopIterationцикл завершается без ошибки.
Сравнение итератора и итерируемого объекта
| Характеристика | Итерируемый объект | Итератор |
|---|---|---|
| Методы | __iter__() | __iter__() и __next__() |
| Создание | списки, строки, кортежи, множества, словари | iter(iterable) |
next() | нельзя вызывать напрямую | возвращает следующий элемент |
| Хранение данных | все элементы сразу | только ссылка и позиция |
| Перебор | можно много раз | однократно |
| Конец данных | не зависит от StopIteration | вызывает StopIteration |
Как проверить, кто перед вами
Абстрактные типы из collections.abc позволяют проверить объект напрямую:
from collections.abc import Iterable, Iterator
numbers = [1, 2, 3]
print(isinstance(numbers, Iterable)) # True — по списку можно пройти
print(isinstance(numbers, Iterator)) # False — сам список позицию не помнит
iterator = iter(numbers)
print(isinstance(iterator, Iterable)) # True
print(isinstance(iterator, Iterator)) # True
print(iter(iterator) is iterator) # True — итератор возвращает сам себя
Отсюда правило: каждый итератор является итерируемым объектом, но не каждый итерируемый объект — итератор. Список можно перебирать бесконечно много раз именно потому, что сам он позицию не хранит — на каждый for создаётся новый итератор.
Свой итерируемый объект и свой итератор
Когда состояние обхода вынесено в отдельный класс, объект можно перебирать повторно и даже параллельно — как обычный список:
from collections.abc import Iterable, Iterator
class MyRange(Iterable):
"""Хранит настройки диапазона."""
def __init__(self, start: int, end: int) -> None:
self.start = start
self.end = end
def __iter__(self) -> "MyRangeIterator":
return MyRangeIterator(self.start, self.end) # каждый раз новый итератор
class MyRangeIterator(Iterator):
"""Хранит позицию конкретного обхода."""
def __init__(self, start: int, end: int) -> None:
self.current = start
self.end = end
def __iter__(self) -> "MyRangeIterator":
return self
def __next__(self) -> int:
if self.current >= self.end:
raise StopIteration
result = self.current
self.current += 1
return result
numbers = MyRange(1, 5)
print(list(numbers)) # [1, 2, 3, 4]
print(list(numbers)) # [1, 2, 3, 4] — второй обход снова работает
first = iter(numbers)
second = iter(numbers)
print(next(first)) # 1
print(next(first)) # 2
print(next(second)) # 1 — у second собственное состояние
Где что лежит:
MyRangeхранит неизменные настройки —startиend;MyRangeIteratorхранит изменяемую позицию —current;- у каждого итератора собственное состояние, поэтому обходы не мешают друг другу.
Если же класс совмещает обе роли (__iter__ возвращает self), объект перебирается только один раз — после StopIteration он исчерпан.
Модуль itertools
Модуль itertools предоставляет эффективные инструменты для работы с итераторами: создание бесконечных последовательностей, объединение коллекций, фильтрацию и комбинаторику — без лишнего расхода памяти.
count(start=0, step=1)— бесконечная арифметическая прогрессия.cycle(iterable)— бесконечное повторение элементов.chain(*iterables)— последовательный перебор нескольких коллекций.product(*iterables, repeat=1)— декартово произведение.permutations(iterable, r=None)— все упорядоченные перестановки длиныr.
Генераторные выражения
Генераторное выражение — способ создания последовательности значений без предварительного вычисления всех элементов. Оно похоже на списковое включение, но использует круглые скобки (...) и возвращает объект-генератор.
Генератор является итератором: поддерживает next(), исчерпывается после одного прохода и не поддерживает индексацию.
| Характеристика | Списковое включение [...] | Генераторное выражение (...) |
|---|---|---|
| Объект | list | generator |
| Создание элементов | все сразу | по запросу |
| Память | хранит всю последовательность | хранит правило генерации |
| Доступ | индексация, срезы | только next() или for |
| Применение | малые и средние данные | большие данные, потоки |
Генераторные выражения удобно передавать в функции, ожидающие итерируемый объект:
words = ["apple", "Banana", "cherry", "Apricot"]
print(any(word[0].isupper() for word in words)) # True
print(all(len(word) > 3 for word in words)) # True
Оценка потребления памяти
Функция sys.getsizeof(obj) возвращает размер объекта в байтах. С её помощью можно сравнить, сколько памяти занимают список и генератор:
import sys
list_comp = [x ** 2 for x in range(10 ** 6)]
gen_expr = (x ** 2 for x in range(10 ** 6))
print(sys.getsizeof(list_comp)) # несколько мегабайт
print(sys.getsizeof(gen_expr)) # десятки байт
typing.Iterator, typing.Iterable, collections.abc.Iterator) выходят за рамки исходного материала урока. Используйте их для самодокументирования кода, но проверьте актуальные рекомендации в документации Python.