Привет, Хабр! Модуль itertools мне известен многим вдоль и поперёк. Ну действительно, что там сложного? Пара функций вроде chain да product, и кажется, что ничего нового для себя уже не найти. Однако стоит копнуть глубже, и выясняется, что у itertools есть немало нюансов и даже новых возможностей, появившихся в свежих версиях. В этой статье рассмотрим многие функции itertools: от базовых до самых интересных.
Бесконечные итераторы: count, cycle, repeat
Начнём с простого. В itertools есть несколько функций, создающих бесконечные последовательности. Самые известные — это count, cycle и repeat. Бесконечные итераторы генерируют данные без конца, пока вы их не остановите сами, например, через break в цикле или срезом islice. Такие итераторы хороши, например, для генерирования бесконечного потока данных или повторения чего‑то до бесконечности (не забудьте поставить условие выхода!). Вглянем на каждый из них.
itertools.count(start=0, step=1)бесконечная арифметическая прогрессия. Вы подаёте начальное значение и шаг, а на выходе получаете итератор, который будет выдаватьstart, start+step, start+2*step, ...до бесконечности. Интересно, когда нужен бесконечный поток чисел: например, вместоfor i in range(n)можно делатьfor i in count()с ручным обрывом или использоватьzip/isliceдля ограничений. Пример использования:
from itertools import count
prog = count(2, 5)
print(next(prog), next(prog), next(prog))
# Вывод: 2 7 12
count(2, 5) создаёт последовательность 2, 7, 12,... (увеличивая на 5). Без ограничения она шла бы бесконечно.
itertools.cycle(iterable)зацикливание последовательности. Берёт любой итерируемый объект (список, строку и тому подобное) и возвращает итератор, который бесконечно повторяет его элементы по кругу. Например, можно бесконечно итерироваться по циклу состояний"red", "green", "blue"или по списку задач, реализуя круговой обход. Пример:
from itertools import cycle
colors = cycle(["red", "green", "blue"])
print(next(colors), next(colors), next(colors), next(colors))
# Вывод: red green blue red
В примере список ["red", "green", "blue"] повторяется заново, когда доходит до конца.
itertools.repeat(element, n=None)повторяет заданный объект. По дефолту повторяет бесконечно, но если указать параметрn, то ровноnраз. Часто полезно для передачи константного значения куда‑нибудь. Можно сделатьrepeat(0, 5)чтобы получить пять нулей подряд. Или использовать бесконечныйrepeatвместе сmap, чтобы применить функцию с неизменным аргументом. Пример простого применения:
from itertools import repeat
print(list(repeat('A', 4)))
# Вывод: ['A', 'A', 'A', 'A']
Здесь repeat('A', 4) выдал список из четырёх символов 'A'. Без второго аргумента repeat('A') генерировал бы 'A' до бесконечности.
Сами по себе эти бесконечные итераторы никогда не завершаются, поэтому применять их надо в комбинации с другими инструментами. Например, можно взять next() определенное число раз, как мы сделали выше, или использовать функцию itertools.islice для ограниченного извлечения элементов. Бесконечные итераторы прекрасно сочетаются с другими функциями itertools, мы это еще увидим ниже по статье.
Объединение последовательностей: chain
Очень часто возникает задача соединить несколько последовательностей подряд. Конечно, можно просто пройтись циклом по каждой или сложить списки, но itertools.chain делает это лениво и эффективно. Функция chain(*iterables) принимает несколько итерируемых объектов и возвращает итератор, последовательно выдающий все элементы из первого, затем из второго и т.д, как будто это одна длинная последовательность. Пример:
from itertools import chain
list1 = [1, 2, 3]
list2 = [4, 5]
list3 = [6, 7, 8]
print(list(chain(list1, list2, list3)))
# Вывод: [1, 2, 3, 4, 5, 6, 7, 8]
Результат — все три списка соединены в один поток. chain не создает новый список в памяти, а проходит по исходным итераторам последовательно. Это применимо для конкатенации ленивых последовательностей (генераторов), которые нельзя просто так взять и сложить оператором +.
Для удобства есть и альтернативный конструктор chain.from_iterable(iterable). Он принимает один аргумент, итерируемый объект, каждый элемент которого сам является итерируемым. Другими словами, chain.from_iterable раскрывает вложенный уровень.
Этот вариант хорош, когда у вас есть, скажем, список списков и вы хотите итерироваться по всем их элементам как по одному длинному списку. Пример эквивалентен приведённому выше, но с использованием одного списка списков:
nested = [[1, 2], [3, 4], [5, 6]]
print(list(chain.from_iterable(nested)))
# Вывод: [1, 2, 3, 4, 5, 6]
Вместо передачи трёх списков в chain передаем один список nested, в котором лежат наши списки. Результат тот же, все элементы последовательностей извлечены подряд. Конструкция chain.from_iterable часто применяется для флаттенинга (выравнивания) вложенных структур, она по сути реализует концепцию flatten (сплющивания вложенной последовательности).
Параллельный обход нескольких последовательностей: zip_longest
Другой тип комбинации это параллельный обход сразу нескольких последовательностей. Для этого в Python есть встроенная функция zip, но она останавливается, как только самая короткая последовательность закончилась. В модуле itertools же имеется расширенная версия — zip_longest. Эта функция позволяет итерироваться по нескольким последовательностям до конца самой длинной, заполняя недостающие значения для более коротких последовательностей специальным значением fillvalue (по умолчанию None).
Посмотрим на примере:
from itertools import zip_longest
a = [1, 2, 3]
b = ['a', 'b']
print(list(zip_longest(a, b, fillvalue='?')))
# Вывод: [(1, 'a'), (2, 'b'), (3, '?')]
Здесь список a длиной 3 и список b длиной 2 объединяются парами. Обычный zip(a, b) вернул бы только [(1, 'a'), (2, 'b')], отбросив лишний элемент 3 из первого списка. А вот zip_longest продолжил до конца более длинного списка a, подставив для отсутствующего элемента из b значение '?'. Таким образом, на выходе получили (3, '?') в третьей позиции. Если fillvalue не указать, там был бы None.
Стоит заметить, что для корректной работы с бесконечными последовательностями zip_longest тоже лучше ограничивать с помощью каких‑нибудь условий. Если среди аргументов есть бесконечный итератор, то zip_longest сам никогда не завершится, так что его нужно оборачивать, например, в islice или takewhile.
Кстати, начиная с Python 3.10 появился параметр strict у встроенной функции zip (не из itertools). Если вызвать zip(..., strict=True), то интерпретатор будет ожидать, что все последовательности одной длины, и бросит исключение, если это не так. Это противоположная логика по сравнению с zip_longest, вместо заполнения пропусков вы наоборот требуете, чтобы их не было. Имейте в виду эту возможность, хотя она и живёт вне модуля itertools.
Фильтрация и отбор элементов
Переходим к функциям для фильтрации последовательностей. В Python есть встроенные filter и генераторы‑выражения вроде [x for x in data if cond(x)]. Itertools предоставляет свои удобные инструменты для распространённых случаев: фильтрация с отрицанием условия, фильтрация по булевой маске, пропуск начала последовательности по условию и так далее Рассмотрим их по очереди: dropwhile, takewhile, filterfalse и compress.
-
itertools.dropwhile(predicate, iterable)пропускает элементы, пока предикат истинен, и как только встречается первый элемент, для которогоpredicateвозвращает False, начинает выдавать все последующие элементы итератора (включая тот самый элемент, на котором прервалось условие). Проще говоря,dropwhileотбрасывает начало последовательности, пока выполняется условие, а потом возвращает остаток. Пример: пусть у нас есть числа, и мы хотим отбросить стартовую часть, пока числа меньше 5:from itertools import dropwhile nums = [1, 4, 6, 3, 8] print(list(dropwhile(lambda x: x < 5, nums))) # Вывод: [6, 3, 8]Сначала проверяются
1и4предикатx < 5для них True, поэтому они отбрасываются. На числе6условие впервые дает False (6 не меньше 5), на этом прекращается отбрасывание.dropwhileвыдаст этот элемент6и всё, что после него (3, 8), не глядя уже на условие. Обратите внимание: как только условие перестало выполняться, никакие дальнейшие элементы больше не проверяются, все сразу возвращаются как есть. -
itertools.takewhile(predicate, iterable)противоположная логика: он будет выдавать элементы из последовательности, пока предикат True, а как только встретит первый False — остановится и завершит итерацию. Т.еtakewhileберёт начало последовательности, удовлетворяющее условию. Для нашего списка чисел продемонстрируем:from itertools import takewhile nums = [1, 4, 6, 3, 8] print(list(takewhile(lambda x: x < 5, nums))) # Вывод: [1, 4]Здесь из списка берутся
1и4, потому что они <5. На числе6предикат возвращает False, поэтомуtakewhileпрекращает работу и не включает этот элемент и всё, что после него. Элемент, на котором условие впервые провалилось (6), уже был прочитан итератором и потерян для дальнейшего использования. Если его нужно ещё обработать внеtakewhile, придётся извращаться, например, ручным циклом или воспользоваться готовым рецептом изmore-itertools(есть утилитаbefore_and_after). Обычно же это не проблема. -
itertools.filterfalse(predicate, iterable)почти то же самое, что встроенныйfilter, только пропускает элементы, для которых предикат вернул False. Можно думать о нём как о фильтре с отрицанием или как о «оставить только те, что не удовлетворяют условию». Например, выберем из диапазона чисел только нечётные, отфильтровав всеx % 2 == 0(то есть чётные) как нежелательные:from itertools import filterfalse print(list(filterfalse(lambda x: x % 2 == 0, range(10)))) # Вывод: [1, 3, 5, 7, 9]Результатом будут только нечётные числа от 0 до 9. Конечно, то же самое можно было получить через обычный
filter(lambda x: x % 2 != 0, ...)или генератор, но иногда удобнее читать именноfilterfalseс условием без отрицания внутри.Если
predicateне задан, то поведение такое: берутся элементы, которые сами по себе ложны (как если бы предикат былbool(x) == False). Это аналог встроенной функцииitertools.filterfalseобычно используют нечасто, но знать полезно. -
itertools.compress(data, selectors)фильтрация по готовой маске. Эта функция берёт два итерируемых объекта:dataиselectors, и возвращает элементы изdata, для которых соответствующий элемент вselectorsявляется True (или 1). Проходит по обоим параллельно, останавливается как только одна из последовательностей закончилась. Проще говоря, вы подаёте маску из булевых значений, указывающую какие элементы оставить. Пример:from itertools import compress letters = "ABCDEF" mask = [1, 0, 1, 0, 0, 1] print(list(compress(letters, mask))) # Вывод: ['A', 'C', 'F']Маска
[1,0,1,0,0,1]означает «взять 1-й элемент, пропустить 2-й, взять 3-й, пропустить 4-й и 5-й, взять 6-й». Именно это мы и видим в результате: остались 'A', 'C', 'F'. Вместо 1/0 можно использовать True/False, эффект будет тот же. Функцияcompressудобна, когда у вас уже есть готовый список‑фильтр. Если же нужно фильтровать по условию, проще использоватьfilter/filterfalse.
Вместе эти инструменты покрывают большую часть задач по выборке элементов из последовательности. Стоит подчеркнуть: они тоже работают лениво, т.е не создают копий списков в памяти, а выдают по одному элементу на лету.
Преобразование последовательностей: starmap
Следующая функция — про преобразование (маппинг) последовательностей, особенно хорошее, когда данные уже представлены в виде кортежей. Речь о itertools.starmap. По смыслу она похожа на встроенную map, но отличается тем, что предназначена для функций, принимающих несколько аргументов. starmap(func, iterable) ожидает, что каждый элемент в iterable это итерируемый контейнер (например, кортеж), и будет вызывать func(*element) для каждого из них. Если говорить образно, starmap распаковывает элементы перед передачей в функцию (отсюда и «star» — тот самый оператор * в Python).
Когда это может пригодиться? Представьте, что у вас есть список точек на плоскости, представленных как пары координат, и вы хотите получить, скажем, их сумму или другую функцию от двух параметров. Можно, конечно, сделать генератор вроде (f(x, y) for (x, y) in points), но starmap позволяет выразить это без лишнего синтаксиса. Посмотрим пример:
from itertools import starmap
points = [(1, 2), (3, 4), (5, 6)]
# Посчитаем сумму координат каждой точки
print(list(starmap(lambda x, y: x + y, points)))
# Вывод: [3, 7, 11]
Функция‑лямбда берёт два аргумента, а каждый элемент из points — кортеж из двух чисел. starmap распаковывает кортежи и подставляет в функцию, благодаря чему мы получаем [1+2, 3+4, 5+6] = [3, 7, 11].
Аналогично можно, например, применять math.pow к списку пар чисел: starmap(pow, [(2,5), (3,2)]) даст [32, 9] (то есть 2^5 и 3^2). Конечно, ту же задачу можно решить и списочным выражением, но starmap иногда делает код более лаконичным и читаемым, особенно если уже имеется готовый список кортежей.
Накопление результата: accumulate
Теперь перейдём к инструменту, который часто оказывается недооценённым itertools.accumulate. Он позволяет получить промежуточные накопленные результаты при последовательной обработке данных. Проще всего думать о нём как об аналоге функции reduce, но который не только возвращает финальное значение, а выдаёт все промежуточные вычисления.
По дефолту accumulate(iterable) будет складывать числа, генерируя подряд частичные суммы (prefix sum). Например, для списка [1,2,3,4,5] он выдаст [1, 3, 6, 10, 15]. Действительно: первый элемент 1, потом 1+2=3, затем 3+3=6, и так далее (это треугольные числа). Покажем на практике:
from itertools import accumulate
data = [1, 2, 3, 4, 5]
print(list(accumulate(data)))
# Вывод: [1, 3, 6, 10, 15]
Однако сила accumulate не только в суммировании. У него есть опциональный параметр func, с помощью которого можно задать любую другую бинарную функцию для накопления. Например, можно собрать накопительное произведение или следить за текущим максимумом. Вот несколько вариантов:
import operator
data = [1, 2, 3, 4, 5]
print(list(accumulate(data, initial=100)))
# Вывод: [100, 101, 103, 106, 110, 115]
print(list(accumulate(data, operator.mul)))
# Вывод: [1, 2, 6, 24, 120]
Первый вызов показал, как работает параметр initial: мы задали начальное значение 100, и генератор начал с него, а затем прибавлял элементы списка. В результате последовательность стала на один элемент длиннее исходной: начали с 100, потом 100+1=101, 101+2=103 и так далее
Второй вызов использует operator.mul в качестве функции накопления это приводит к перемножению элементов. Как видим, получили последовательность частичных произведений: 1, 12=2, 23=6, 64=24, 245=120. Это, по сути, факториалы для исходного списка. Аналогично можно передать min или max вместо operator.mul, чтобы на лету вычислять минимумы или максимумы prefix‑отрезков списка.
Таким образом, accumulate сам по себе универсал для сканирования последовательности с накоплением результата. В функциональном программировании такая операция известна как scan или prefix reduce. Если вам нужен только финальный итог, есть functools.reduce, но когда важно получить все шаги, поможет accumulate. Из интересного применения: с его помощью можно, например, построить амортизационную таблицу платежей или проследить баланс счета, накапливая проценты и вычитаемые суммы.
Смежные пары: pairwise
Поговорим о функции, которая сравнительно недавно пополнила семейство itertools. Это itertools.pairwise. Она позволяет итерироваться по последовательности с шагом 1, получая на каждой итерации кортеж из двух соседних элементов (пар). Проще всего представить: мы скользим окном размера 2 по iterable. Классический пример — нужно пройтись по последовательности попарно, чтобы сравнить текущий и следующий элементы. Раньше для этого либо писали индексы, либо извращались с zip.
С появлением pairwise всё тривиально:
from itertools import pairwise
print(list(pairwise("ABCDE")))
# Вывод: [('A', 'B'), ('B', 'C'), ('C', 'D'), ('D', 'E')]
Результатом получим последовательность перекрывающихся пар: ('A','B'), потом ('B','C') и так далее. Как видно, пар на одну меньше, чем элементов в исходной строке. Если исходный iterable содержит меньше 2 элементов, pairwise вернёт пустой итератор. Эта функция ведёт своё происхождение из рецептов itertools, долгое самостоятельно писали генератор для таких целей, и решили добавить готовое решение в стандартную библиотеку.
Где это может пригодиться? Масса случаев. Например, нужно найти монотонные участки в списке чисел, удобно смотреть на пары (prev, next). Или, скажем, вы хотите вычислить разницу между соседними измерениями в списке показаний датчика: опять же pairwise даст вам соседние значения, из которых легко получить разности. Раньше для этого делали что‑то вроде zip(data, data[1:]) или комбинацию tee и islice. Кстати, эквивалент через tee выглядел бы так:
a, b = tee(data)
next(b, None)
pairs = zip(a, b)
pairwise делает то же самое, только внутрях и оптимально. В общем, если раньше вы думали, как идеально пройтись «по два элемента», теперь ответ один, используйте pairwise.
Для общего развития: функция pairwise это частный случай более общей задачи скользящего окна (sliding window).
Разбиение на фиксированные блоки: batched
Ещё одна не побоюсь этого слова, прикольная функция itertools.batched. Она решает задачу разбиения последовательности на чанки фиксированной длины.
Сигнатура: itertools.batched(iterable, n, *, strict=False). На вход подаём итерируемый источник и размер пачки n. Функция возвращает итератор, выдающий кортежи длиной n, полученные из исходных данных. Последний кортеж может оказаться короче, если данных не хватит до полного блока — это поведение по умолчанию. Если же установить strict=True, то в случае неполного хвоста будет выброшено исключение ValueError (можно использовать, когда вы ожидаете, что длина делится нацело, и хотите ошибку если нет).
Пример использования batched: разобьём числа 1..9 на группы по 4:
# Пусть batched уже импортирован из itertools
data = range(1, 10)
print(list(batched(data, 4)))
# Вывод: [(1, 2, 3, 4), (5, 6, 7, 8), (9,)]
Получили три кортежа: первые два по 4 элемента, и последний оставшиеся (9,) из одного элемента. Поскольку strict=False (по дефолту), неполная последняя группа нас не волнует. Если бы стояла опция strict, то при обнаружении, что последний блок размера 1 меньше 4, был бы raise ValueError.
Сам по себеbatched работает лениво: берет по n элементов из итератора, как только набралось n отдаёт кортеж. Реализация аккуратная и использует itertools.islice внутри.
Дублирование итератора: tee
Функция itertools.tee позволяет из одного исходного итератора сделать несколько независимых итераторов. Синтаксис: tee(iterable, n=2) возвращает кортеж из n итераторов, каждый из которых будет выдавать те же элементы исходной последовательности. Под капотом tee реализует буферизацию: элементы исходного итератора сохраняются, пока они не будут прочитаны всеми порождёнными итераторами.
Рассмотрим на примере, чтобы понять поведение:
from itertools import tee
orig = iter([10, 20, 30])
a, b = tee(orig, 2)
print(next(a)) # Читаем первый элемент через итератор a
# Вывод: 10
print(list(b)) # Читаем все элементы через итератор b
# Вывод: [10, 20, 30]
print(list(a)) # Докчитываем итератор a до конца
# Вывод: [20, 30]
Что здесь произошло: взяли исходный итератор по списку [10, 20, 30] и сделали из него два итератора a и b. Когда мы вызвали next(a), он выдал 10. В этот момент tee внутренне сохранил значение 10 для второго итератора b, ведь b же ещё не читал и мог пропустить этот элемент. Далее мы превратили b в список, то есть полностью вычитали все элементы через b. И b получил все три значения: ему сначала отдался сохранённый 10, а потом уже читались 20 и 30 напрямую из исходного итератора. В итоге b напечатал [10, 20, 30]. После этого итератор a уже успел получить 10 ранее, осталось получить через a то, что он ещё не читал — это 20 и 30. Мы их вывели. Как видно, оба итератора в сумме прошли по всем данным, и каждый получил все элементы, просто в разном порядке чтения.
tee вынужден хранить временно элементы, чтобы не потерять их для отстающих итераторов. В худшем случае, если один из полученных итераторов далеко уйдёт вперёд, а другой будет потребляться медленно или вообще потом, то все пройденные элементы будут копиться в памяти.
При неосторожном использовании это может привести к большому потреблению памяти. Если вы планируете, что один итератор из пары tee прочитает почти всё, прежде чем вы начнёте читать второй, то вместо tee лучше сначала сохранить данные в список. Проще говоря, tee полезен, когда оба (или все N) результирующие итератора будут потребляться более‑менее синхронно.
Несколько дополнительных нюансов:
Итераторы от
teeне потокобезопасны, не стоит их использовать из разных потоков, могут возникнуть проблемы. Обычно в Python GIL всё равно, но вдруг.Если вызвать
teeсn=0, получите пустой кортеж. Сn=1вернётся один итератор, эквивалентный исходному (по сути бесполезно).teeможно вызывать и на уже тиковом итераторе (который сам является tee‑объектом), тогда он оптимизируется и не будет строить лишние буферы, а просто вернёт дополнительные ответвления от исходного буфера. Это редко нужно, но на уровне реализации крутой момент.
Группировка элементов: groupby
Функция itertools.groupby позволяет сгруппировать последовательность по определённому ключу. Однако, если вы привыкли к SQL или Pandas, надо сразу оговориться: поведение groupby в Python не такое, к чему склоняет слово «group by». Оно группирует только подряд идущие элементы с одинаковым ключом. Как сказано в документации, это похоже на утилиту uniq в Unix, группы формируются при изменении ключа, поэтому обычно требуется, чтобы данные были предварительно отсортированы по этому ключу. В отличие от SQL, где одинаковые значения сгруппируют вне зависимости от порядка, здесь порядок играет большущую роль.
Синтаксис: groupby(iterable, key=None). Возвращается итератор, порождающий пары (k, group), где k значение ключа, а group итератор по группе элементов. Параметр key функция, вычисляющая ключ по элементу (аналог ключа сортировки). Если не указать, используется сам элемент как ключ.
Допустим, у нас есть список имён, и мы хотим сгруппировать их по первой букве. Сначала отсортируем список по первой букве, затем применим groupby:
from itertools import groupby
names = ["Alice", "Anna", "Bob", "Billy", "Cathy", "Carlos", "Dmitri"]
names.sort(key=lambda x: x[0])
for letter, group in groupby(names, key=lambda x: x[0]):
print(letter, list(group))
# Вывод:
# A ['Alice', 'Anna']
# B ['Bob', 'Billy']
# C ['Cathy', 'Carlos']
# D ['Dmitri']
Получили 4 группы: все имена на 'A', на 'B', на 'C' и на 'D'. Внутри группы находятся имена, следующие друг за другом в отсортированном списке. Обратите внимание: groupby сам не собирает элементы группы в список, мы обернули list() вокруг group, чтобы показать их сразу. В реальности group это итератор, который делит один и тот же исходный поток с объектом groupby. Как только вы начали вытягивать элементы из одной группы, исходный итератор идёт вперёд до конца этой группы. Если попробовать потом где‑то ещё использовать тот же итератор (например, снова обойти группой), предыдущие элементы уже прошли. Поэтому, если данные из группы нужны позже, их стоит сохранить.
Теперь про важность предварительной сортировки. Посмотрим, что будет, если не сортировать. Возьмём последовательность строк и сгруппируем по первой букве в том порядке, как они идут:
items = ["AA", "BA", "AB", "BB"]
for k, g in groupby(items, key=lambda x: x[0]):
print(k, list(g))
# Вывод:
# A ['AA']
# B ['BA']
# A ['AB']
# B ['BB']
Получилось четыре группы, хотя уникальных первых букв всего две ('A' и 'B'). Это потому, что последовательность не сгруппирована по ключу изначально: первые два элемента дали группы 'A' и 'B', потом снова встретилась строка на 'A' и groupby открыл новую группу 'A', и затем группа 'B'. Если бы задача стояла именно собрать все 'A' вместе и все 'B' вместе, нужно было сначала отсортировать список по первой букве:
for k, g in groupby(sorted(items, key=lambda x: x[0]), key=lambda x: x[0]):
print(k, list(g))
# Вывод:
# A ['AA', 'AB']
# B ['BA', 'BB']
Теперь группы корректные. Поэтому для полезного применения groupby обычно надо отсортировать данные по тому же ключу. Либо если данные уже приходят упорядоченными по группам, тогда всё сразу работает.
Функция groupby очень эффективна, так как выполнена на C уровне и не хранит никаких больших структур, она проходит один раз по данным. Однако использовать её надо с осторожностью из‑за вышеописанного поведения. Если вы хотели глобально сгруппировать, а не подряд идущие, то лучше использовать другие методы, например, словарь списков или itertools.groupby вкупе с сортировкой, как мы сделали.
groupby хорош, когда у вас поток данных уже упорядочен естественным образом. Например, читаем из файла записи, сгруппированные по разделам, можно сразу применять groupby, не загружая весь файл в память. Или при выборке из базы с сортировкой по нужному полю, а затем группировке в Python.
Стоит также помнить, что группы, которые возвращает groupby, живут до тех пор, пока вы не начали итерацию следующей группы. Как только вы перешли к следующей, предыдущая закрывается и становится недоступной (они используют один и тот же итератор‑источник). Поэтому, если нужно работать с несколькими группами параллельно, придётся скопировать данные, например, сделав list(g) как мы делали.
Комбинаторные итераторы: product, permutations, combinations
Теперьо генерации разных сочетаний из элементов. Модуль itertools предоставляет сразу несколько функций для перебора комбинаций и перестановок:
itertools.product(*iterables, repeat=1)декартово произведение (прямое произведение) последовательностей. То же, что вложенные циклы: берёт по одному элементу из каждой последовательности и образует кортеж. Параметрrepeatпозволяет повторить одну и ту же последовательность несколько раз. Например,product(A, B)эквивалентно парам(x, y)приxизAиyизB. Аproduct(A, repeat=3)даст все кортежи длины 3 из элементовAто есть прямое произведение A × A × A.itertools.permutations(iterable, r=None)перестановки без повторений. Генерирует всевозможные кортежи длиныrиз элементовiterable, не повторяя элементы. Еслиrне указан, по дефолту берется длина всей последовательности (то есть полные перестановки). Например,permutations([1,2,3], 2)даст все упорядоченные пары без повторов из {1,2,3}. Количество таких кортежей равноn!/(n-r)!. Элементы считаются уникальными по индексу, то есть если в исходном списке есть дубликаты значений, они всё равно считаются разными позициями и могут образовывать вроде бы повторяющиеся комбинации.itertools.combinations(iterable, r)сочетания без повторений. В отличие от перестановок, здесь порядок не учитывается, то есть (A,B) и (B,A) считаются одной комбинацией (будет выдан только в каком‑то одном порядке, по факту — в порядке, соответствующем сортировке по индексам). Генерирует все комбинации длиныrиз данной последовательности. Число комбинаций равноC(n, r)(биномиальный коэффициент). Порядок в самом выходном кортеже соответствует порядку элементов в исходном iterable.itertools.combinations_with_replacement(iterable, r)сочетания с повторениями. Это похоже на обычные combinations, но один элемент может включаться в комбинацию несколько раз. Например, комбинации с повторениями из['A','B']по 2 даст('A','A'), ('A','B'), ('B','B'). Тут'A','B'один раз, а'A','A'и'B','B'где элемент повторяется. Количество таких комбинаций равноC(n+r-1, r)(формула сочетаний с повторением). В остальном поведение аналогично, возвращаются отсортированные по порядку исходных элементов кортежи.
Эти функции очень удобны для всевозможных переборов, генерации тестовых данных, решения комбинаторных задач. Однако нужно понимать их затраты: число результатов растёт комбинаторно, и можно легко сгенерировать гигантское количество комбинаций, исчерпав память или время. Впрочем, сами они ленивые, результат отдают по одному, так что если не превращать в список, то хотя бы память мгновенно не взорвётся (но перебор всё равно может быть астрономически долгим).
Рассмотрим примеры для наглядности:
from itertools import product, permutations, combinations, combinations_with_replacement
print(list(product('AB', 'xy')))
# Вывод: [('A', 'x'), ('A', 'y'), ('B', 'x'), ('B', 'y')]
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')]
print(list(combinations_with_replacement('AB', 2)))
# Вывод: [('A', 'A'), ('A', 'B'), ('B', 'B')]
Объясним полученные результаты:
Для
product('AB', 'xy')мы получили все пары, где первая буква из'AB', вторая из'xy'. Всего 2*2 = 4 комбинации.Для
permutations('ABC', 2)результат, все упорядоченные пары из трёх букв, их 3P2 = 6 штук. Обратите внимание: ('A','B') и ('B','A') присутствуют как разные кортежи.Для
combinations('ABC', 2)все неупорядоченные пары из трёх букв, их 3C2 = 3 штуки. Тут уже ('A','B') и ('B','A') считаются одним сочетанием и в выходе представленны как ('A','B') (в лексикографическом порядке).Для
combinations_with_replacement('AB', 2)все пары из {'A','B'}, где элементы могут повторяться. Получили 3 комбинации: обе буквы 'A', потом 'A' с 'B', потом обе 'B'. ('B','A') не выводится отдельно, так как это то же самое, что ('A','B') с точки зрения сочетаний.
Все эти итераторы очень хороший инструмент в умелых руках. Можно, например, быстро перебрать пространство параметров теста, сгенерировать все пароли заданной длины из символов (осторожно), реализовать брутфорс комбинаций и тому подобное Благодаря ленивости, можно сразу фильтровать или ограничивать генерируемые комбинации, не строя их все целиком. Например, можно обернуть itertools.islice вокруг product чтобы генерировать только первые N комбинаций из миллиарда.
И ещё один плюс: эти функции реализованы на C, так что обычно работают быстрее эквивалентных питонских вложенных циклов или рекурсивных генераторов.
Казалось бы, модуль небольшой, но в нём скрывается целая куча итераторв, позволяющая строить из простых блоков очень гибкие конвейеры обработки данных. Многие функции по началу кажутся избыточными («зачем нужен compress, если есть генераторы?»), но с опытом начинаешь ценить лаконичность и эффективность решений, которые предоставляет itertools. Многие из этих функций написаны самим Рэймондом Хеттингером и другими core‑разработчиками, и реализованы на C, их сложно превзойти по производительности на чистом питоне.
Надеюсь, этот обзор оказался для вас полезным. Если я что‑то упустил или у вас есть свои любимые фичи с itertools, мело делитесь в комментариях. Спасибо за внимание и да пребудет с вами сила.
В рамках курса Python Developer. Professional рассматриваются не только продвинутые аспекты стандартной библиотеки, но и такие темы, как архитектура приложений, оптимизация производительности и создание отказоустойчивых систем. Это возможность систематизировать свой опыт и познакомиться с профессиональными инструментами и подходами в разработке.
Пройдите бесплатное тестирование по курсу, чтобы оценить свои знания и навыки. А еще — приходите на открытые уроки, которые преподаватели курса проведут бесплатно:
27 октября: «Как не нужно писать на Python». Записаться
10 ноября: «Асинхронное взаимодействие в Python на примере RabbitMQ». Записаться
18 ноября: «Научим нейросеть распознавать рукописные буквы прямо на занятии». Записаться
Комментарии (3)

domix32
20.10.2025 16:48Мне интересно, насколько это вообще востребовано. Какие-нибудь permutations, product, groupby или batched я могу представить, но вот что до остального - не представляю, чтобы кому-то что-то из этого понадобилось вместо обычных list comprehensions. Для чего-то продвинутого и производительно есть какой-нибудь pandas.
fireSparrow
Кликбейт. По заголовку и вступлению ожидал, что увижу какие-то неожиданные нюансы при работе с iterttols, а вы просто пересказали доку.
GCU
С версии 3.12 в itertools наконец-то появился batched, до этого писали свои костыли со слайсами, всякие chunks и т.п. чтобы делить на куски. Мелочь, но приятно