Реклама
Меморина
Меморина
Меморина

itertools в Python: ленивые итераторы без лишних циклов

Модуль itertools — это не просто набор функций, а способ мыслить о данных как о потоке. Разбираем, как писать меньше циклов и не держать миллионы объектов в памяти одновременно.

Обложка: itertools в Python: ленивые итераторы без лишних циклов

Если ваш код регулярно превращается в пять вложенных for и огромный список, который держится в памяти только ради того, чтобы тут же быть выброшенным в мусор, — скорее всего, вам не хватает itertools. Этот модуль стандартной библиотеки Python собрал ленивые итераторы, которые превращают обработку данных в конвейер: элементы поступают, преобразуются и уходят дальше без лишних промежуточных коллекций.

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

Ключевые выводы

itertools — модуль стандартной библиотеки Python, который предоставляет ленивые итераторы для композиции цепочек обработки данных.

Ленивость означает, что элементы создаются по запросу, поэтому можно работать с большими файлами и бесконечными потоками, не загружая всё в память.

Функции разделены на три группы: бесконечные итераторы (count, cycle, repeat), конечные итераторы (chain, islice, groupby и др.) и комбинаторные (product, permutations, combinations).

Многие рутинные задачи — flatten, батчинг, скользящее окно, группировка — решаются в одну строку, если знать правильную комбинацию функций.

Подводные камни: groupby требует предварительной сортировки по тому же ключу; итераторы одноразовые; tee() может неэкономно расходовать память.

Зачем вообще ленивые итераторы?

Обычный подход в Python — сгенерировать список и пройтись по нему циклом. Это просто и читаемо, пока данных немного. Когда файл весит несколько гигабайт, а строки приходят из сети, список становится дорогим: он требует памяти и времени, хотя в каждый момент вам нужен лишь один текущий элемент.

Ленивый итератор не строит коллекцию целиком. Он помнит текущее состояние и умеет «добыть» следующий элемент. Поэтому itertools работает с любыми итерируемыми источниками — файлами, генераторами, сетевыми потоками — и не требует, чтобы весь объём данных поместился в оперативную память.

			from itertools import islice

# Бесконечный итератор, но память не растёт
from itertools import count

for i in islice(count(1), 5):
    print(i)
# 1 2 3 4 5
		
Память vs скорость:
Ленивость экономит память, но не всегда ускоряет код. Если данные всё равно нужны целиком — например, для сортировки — список может оказаться быстрее. Используйте итераторы там, где важна потоковая обработка.

Три семейства инструментов

Документация Python делит функции модуля на три группы. Такое разделение помогает быстро выбрать инструмент: нужна бесконечная последовательность, преобразование конечной или перебор комбинаций.

Бесконечные итераторы: count, cycle, repeat

count — это range без конца. Ему можно задать начальное значение и шаг, и он будет выдавать числа до тех пор, пока его не остановят снаружи. cycle бесконечно повторяет переданную последовательность, а repeat — бесконечно или заданное число раз возвращает один объект.

			from itertools import count, cycle, repeat

# Бесконечный счётчик с шагом 0.5
for x in zip(count(0, 0.5), range(5)):
    print(x)

# Циклическая смена статусов
statuses = cycle(['ok', 'warn', 'fail'])
for _ in range(6):
    print(next(statuses))

# Повтор константы для map
squares = list(map(pow, range(5), repeat(2)))
# [0, 1, 4, 9, 16]
		

Классический трюк — сочетание map и count: map(f, count()) работает как математическое табулирование tabulate(f), знакомое из SML и Haskell.

Конечные итераторы: chain, islice, groupby и другие

Это самая многолюдная группа. Здесь есть функции для склейки последовательностей, фильтрации, нарезки, группировки и пакетной обработки. Их объединяет одно: на вход подаётся конечный или контролируемый итератор, на выходе — тоже итератор.

			from itertools import chain, batched, groupby

# Разворачиваем список списков в плоский итератор
records = [['a', 'b'], ['c'], ['d', 'e', 'f']]
flat = chain.from_iterable(records)
print(list(flat))
# ['a', 'b', 'c', 'd', 'e', 'f']

# Делим данные на партии по 2 элемента
data = range(7)
for batch in batched(data, 2):
    print(batch)
# (0, 1) (2, 3) (4, 5) (6,)
		

batched появился в Python 3.12 и сразу стал незаменимым инструментом: партии запросов к API, пакеты строк для вставки в базу, страницы данных. Последняя партия может быть короче — это поведение по умолчанию.

			from itertools import groupby

# Группировка логов по статусу
log = [
    ('2026-07-01', 'INFO'),
    ('2026-07-02', 'INFO'),
    ('2026-07-03', 'ERROR'),
    ('2026-07-04', 'ERROR'),
    ('2026-07-05', 'INFO'),
]

for status, rows in groupby(log, key=lambda x: x[1]):
    print(status, len(list(rows)))
# INFO 2
# ERROR 2
# INFO 1
		

groupby часто путают с SQL-аналогом, но это не тот же инструмент. Python-версия группирует только подряд идущие одинаковые ключи, поэтому перед ней обычно нужна сортировка по тому же ключу. Если забыть про это, результат покажется случайным.

Комбинаторные итераторы: product, permutations, combinations

Эти функции генерируют декартово произведение, перестановки и сочетания. Они незаменимы в тестировании, алгоритмах на графах, задачах оптимизации и даже в простых играх. Все они ленивые, поэтому можно перебирать комбинации по одной, не строя гигантский список.

			from itertools import product, permutations, combinations

# Все пары значений двух параметров
for a, b in product(['GET', 'POST'], ['/api/v1', '/api/v2']):
    print(a, b)

# Все двухэлементные перестановки
print(list(permutations('ABC', 2)))
# [('A','B'), ('A','C'), ('B','A'), ('B','C'), ('C','A'), ('C','B')]

# Сочетания без учёта порядка
print(list(combinations('ABC', 2)))
# [('A','B'), ('A','C'), ('B','C')]
		

product с аргументом repeat удобен для перебора многомерных конфигураций: product([0, 1], repeat=3) даст все двоичные triples, как в таблице истинности.

Подводные камни, за которые хватаются новички

Несмотря на простоту отдельных функций, у модуля есть несколько особенностей, которые легко превратить в баг.

  • groupby работает только с подряд идущими одинаковыми ключами. Перед вызовом сортируйте данные по тому же ключу, иначе группы разобьются.
  • Итераторы одноразовые. После list(iterator) исходный итератор опустошён, и второй проход по нему даст пустой результат.
  • tee копирует данные во внутренний буфер, пока все производные итераторы не прочитают их. Если один итератор сильно отстаёт, память может расти не хуже списка.
  • zip_longest с бесконечным итератором никогда не остановится. Ограничивайте такие комбинации islice или takewhile.
  • product полностью потребляет входные итераторы, чтобы построить пулы значений. С бесконечными последовательностями его использовать нельзя.

Рецепты: от простого к составному

Документация Python включает раздел рецептов — готовые комбинации функций, которые решают частые задачи. Некоторые из них настолько удобны, что со временем превращаются в полноценные функции модуля: так появились accumulate, compress и pairwise.

			from itertools import islice, chain
from collections import deque

def take(n, iterable):
    """Первые n элементов в виде списка."""
    return list(islice(iterable, n))

def consume(iterator, n=None):
    """Продвинуть итератор на n шагов или до конца."""
    if n is None:
        deque(iterator, maxlen=0)
    else:
        next(islice(iterator, n, n), None)

def flatten(list_of_lists):
    """Снять один уровень вложенности."""
    return chain.from_iterable(list_of_lists)

def sliding_window(iterable, n):
    """Скользящее окно ширины n."""
    it = iter(iterable)
    window = deque(take(n, it), maxlen=n)
    if len(window) == n:
        yield tuple(window)
    for x in it:
        window.append(x)
        yield tuple(window)

print(list(sliding_window(range(5), 3)))
# [(0, 1, 2), (1, 2, 3), (2, 3, 4)]
		

Для sliding_window сейчас часто используют рецепт из документации или аналог из more-itertools, где функция уже реализована и хорошо протестирована.

Часто задаваемые вопросы
1
Чем itertools отличается от генераторных выражений?

Генераторные выражения — это способ создать итератор прямо в коде. itertools предлагает готовые, написанные на C примитивы: они быстрее, проще читаются и хорошо комбинируются друг с другом. Идеально использовать их вместе.

2
Можно ли использовать itertools с бесконечными потоками данных?

Да, но только с функциями, которые не пытаются материализовать весь поток. Подходят count, cycle, repeat, chain, islice, takewhile, dropwhile. Опасны product, permutations, combinations и tee — они либо требуют конечных данных, либо копируют их.

3
Почему groupby не группирует одинаковые элементы, разбросанные по списку?

Python-версия groupby похожа на утилиту uniq в Unix: она группирует только соседние элементы с одинаковым ключом. Чтобы получить поведение SQL GROUP BY, отсортируйте данные по ключу перед вызовом.

4
Когда лучше не использовать itertools?

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

5
Где взять ещё рецепты?

В официальной документации Python есть раздел Itertools Recipes, а в PyPI — пакет more-itertools, который собрал сотни полезных функций поверх itertools.

Выводы

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

Together, they form an iterator algebra making it possible to construct specialized tools succinctly and efficiently in pure Python.
Python documentationdocs.python.org

Источник: itertools — Functions creating iterators for efficient looping. Если в вашем коде до сих пор царят вложенные циклы и огромные списки — попробуйте заменить их на поток. Скорее всего, получится короче, быстрее и понятнее.