Итераторы и генераторы в Python: как экономить память и упростить обработку данных

На чтение
6 мин
Дата обновления
05.11.2026

При обработке большого набора данных важно решить, нужен ли весь результат одновременно. Если задача — последовательно очистить значения, отфильтровать и подсчитать их, промежуточные списки могут быть лишними. Итераторы и генераторы помогают организовать такой поток, но требуют помнить о состоянии и однократном обходе.

Разберём собственный итератор, генераторную функцию и ленивую цепочку обработки строк. Примеры выполнены на Python 3.12. Это учебная логика без обещаний фиксированной экономии памяти для любой программы.

Iterable и iterator: разные роли

Итерируемый объект позволяет получить итератор через iter. Итератор выдаёт очередные значения через __next__ и сообщает об окончании с помощью StopIteration. Его __iter__ возвращает сам итератор. Эти правила описаны в руководстве Python.

Список является итерируемым: каждый новый iter по списку может дать отдельный проход. Но сам итератор списка хранит текущую позицию. Если два участка кода используют один и тот же итератор, они потребляют общий поток, а не независимые копии.

values = [10, 20, 30]
iterator = iter(values)
print(next(iterator))  # 10
print(list(iterator)) # [20, 30]
print(list(iterator)) # []
print(list(values))   # [10, 20, 30]

Здесь список не изменился. Изменилась позиция отдельного объекта iterator. Это различие объясняет множество ошибок, когда второй цикл неожиданно ничего не делает.

Как for скрывает протокол

Цикл for получает итератор и последовательно запрашивает значения, пока они не закончатся. Поэтому в обычном обходе не приходится вручную обрабатывать StopIteration на каждом шаге.

Ручной next полезен для отдельного элемента. Второй аргумент задаёт результат при исчерпании: next(iterator, None) вернёт None, если значений больше нет. Если None может быть настоящим значением потока, выберите другой различимый маркер, например отдельный object().

Не используйте общий перехват всех исключений как способ понять, что поток закончился. Ошибка преобразования числа и нормальное исчерпание означают разные ситуации и требуют разного решения.

Собственный итератор и функция с yield

В авторском классе Countdown состояние хранится в remaining. Каждый __next__ возвращает текущее число и уменьшает остаток. Когда значение стало неположительным, класс сообщает об окончании.

Функция numbers показывает другой способ: она перебирает строки, пропускает пустые и выдаёт целое число через yield. Генератор сохраняет локальное состояние при паузе и продолжает его при следующем запросе. Не нужно писать отдельный класс для каждого такого преобразования.

class Countdown:
    def __init__(self, start):
        self.remaining = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.remaining <= 0:
            raise StopIteration
        value = self.remaining
        self.remaining -= 1
        return value

def numbers(lines):
    for line in lines:
        text = line.strip()
        if text:
            yield int(text)

Выполненная проверка Countdown(3) получила сначала 3, затем оставшиеся [2, 1]. Новый обход того же объекта дал пустой список. Это намеренно одноразовый итератор; для повторения создают новый Countdown.

Генераторный объект также является итератором, но не всякий итератор создан через yield. Поэтому полезно различать протокол потребления и способ реализации конкретного потока.

Выражение генератора и списковое включение

squares_list = [n * n for n in range(5)]
squares_stream = (n * n for n in range(5))

Первое выражение создаёт список результатов. Второе создаёт генераторный итератор, который выдаёт соответствующие значения по запросу. Если потребитель сохраняет каждый результат в новый список, итоговое накопление никуда не исчезает.

Есть важная тонкость официального описания Python: итерируемое выражение в самом левом for вычисляется сразу, и итератор для него тоже создаётся сразу. Остальные выражения вычисляются лениво. Поэтому фраза «у генератора абсолютно всё откладывается до next» неточна.

Например, ссылка на несуществующий источник может дать ошибку при создании выражения, а ошибка в вычислении очередного результата — при потреблении. При отладке нужно знать, на каком из этих этапов вы находитесь.

Ленивая цепочка с filter и map

Возьмём строки с числами 10, −3 и 7, а также пустую строку. Требуется убрать пустое, оставить положительные числа и удвоить их. В выбранном примере неправильная непустая запись считается ошибкой, а не молча пропускается.

raw = [" 10\n", "\n", "-3\n", "7\n"]
positives = filter(lambda value: value > 0, numbers(raw))
doubled = map(lambda value: value * 2, positives)
print(list(doubled))  # [20, 14]
print(list(doubled))  # []

Документация Python описывает map и filter как создающие итераторы. Каждый следующий этап получает значения предыдущего по мере потребления. В этом коде list используется в конце только для показа маленького результата.

Для подсчёта можно потреблять поток без списка, например суммировать единицы для подходящих элементов. Но нельзя сначала полностью вывести doubled, а потом ожидать посчитать его заново: он уже исчерпан. Пересоздайте всю цепочку вместе с источником либо заранее выберите, какие результаты действительно нужно хранить.

Где экономия заканчивается

Маленький генератор не доказывает маленький расход памяти всей программы. Если исходные строки уже загружены в огромный список, этот список существует независимо от ленивого преобразования. Генератор также может сохранять ссылки на объекты в своём состоянии.

Операции, которым нужен весь набор, меняют характер обработки. Превращение в list накапливает элементы; сортировка требует данных для упорядочивания. Если дальнейшему шагу нужна случайная адресация, возможно, сохранённая коллекция будет осмысленным решением.

Для больших данных оценивайте весь путь: получение источника, промежуточные преобразования и потребителя. Построчное чтение файла может быть источником для numbers, но ресурс должен оставаться открытым во время фактического обхода. Руководство Python по вводу-выводу объясняет закрытие файла после блока with.

Проверяйте отложенные ошибки

Выполненный тест numbers со строкой not a number создал генератор, но ValueError возник при next. Это ожидаемо: преобразование int выполняется, когда потребитель доходит до строки.

Чтобы расследовать ошибку в длинной цепочке, временно упростите её и проверьте небольшой набор, сохраняя номер или контекст записи. Не объявляйте выполнение успешным только потому, что генераторный объект был создан без исключения.

Выбирайте список, когда нужны повторные проходы, индексы и небольшой сохранённый результат. Выбирайте последовательную цепочку, когда каждый элемент можно обработать один раз. Главный навык — видеть, что уже вычислено, что ещё ожидает запроса и кто владеет текущей позицией.