📖 Теория: Итераторы и генераторные выражения

⚡ Кратко

Итератор — это объект, который выдаёт элементы коллекции по одному, запоминая текущую позицию.

Итерируемый объект (список, строка, кортеж) умеет возвращать итератор через __iter__(), но сам не является итератором.

Встроенные функции iter() и next() — это «внешний интерфейс» к магическим методам __iter__() и __next__().

Цикл for автоматически вызывает iter(), затем next(), и завершается при StopIteration.

Генераторное выражение в круглых скобках лениво вычисляет значения, экономя память по сравнению со списковым включением.

Проверить роль объекта можно через collections.abc: isinstance(x, Iterable) и isinstance(x, Iterator). Каждый итератор итерируем, но не наоборот.

Если состояние обхода вынести в отдельный класс-итератор, объект можно перебирать повторно и параллельно — как список.

Итератор и итерируемый объект

Итерируемый объект (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: внутри выполняет следующее:

  1. Вызывает iter(iterable), чтобы получить итератор.
  2. Вызывает next(iterator) и передаёт результат в element.
  3. Повторяет шаг 2, пока next() не вызовет StopIteration.
  4. При 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(), исчерпывается после одного прохода и не поддерживает индексацию.

ХарактеристикаСписковое включение [...]Генераторное выражение (...)
Объектlistgenerator
Создание элементоввсе сразупо запросу
Памятьхранит всю последовательностьхранит правило генерации
Доступиндексация, срезытолько 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.