SprintCode.pro

Подготовка к алгоритмическим задачам

Super

Модуль itertools в Python: перебор без лишней памяти

11 мин чтения
python
алгоритмы

Коротко

ФункцияЧто делает
productдекартово произведение (вложенные циклы)
permutationsвсе перестановки
combinationsвсе сочетания
accumulateнакопленные суммы
groupbyгруппировка подряд идущих
chainсклейка последовательностей
isliceсрез для итератора

Главное свойство всех функций модуля: они возвращают итераторы, а не списки. Элементы вычисляются по одному, память не расходуется на весь результат.

Комбинаторные функции

product: замена вложенным циклам

from itertools import product # было for i in range(3): for j in range(3): for k in range(3): ... # стало for i, j, k in product(range(3), repeat=3): ...

Особенно удобно, когда количество вложенных циклов заранее неизвестно:

# все двоичные строки длины 4 for bits in product('01', repeat=4): print(''.join(bits)) # 0000, 0001, 0010, ... # перебор комбинаций из разных наборов sizes = ['S', 'M', 'L'] colors = ['красный', 'синий'] for size, color in product(sizes, colors): print(size, color)

permutations: перестановки

from itertools import permutations list(permutations([1, 2, 3])) # [(1,2,3), (1,3,2), (2,1,3), (2,3,1), (3,1,2), (3,2,1)] list(permutations([1, 2, 3], 2)) # размещения по 2 # [(1,2), (1,3), (2,1), (2,3), (3,1), (3,2)]

Помните про рост: permutations от 10 элементов даёт 3.6 миллиона кортежей, от 12 — почти 480 миллионов. Перебор перестановок реалистичен максимум до 10–11 элементов.

combinations: сочетания

Порядок не важен, элементы идут в исходном порядке:

from itertools import combinations, combinations_with_replacement list(combinations([1, 2, 3], 2))
Пройди собеседование в топ-компанию
Платформа для подготовки

Решай алгоритмические задачи как профи

✓ Популярные алгоритмы✓ Разбор решений✓ AI помощь
Начать сейчас
Программист за работой

[(1,2), (1,3), (2,3)]

list(combinations_with_replacement([1, 2, 3], 2))

[(1,1), (1,2), (1,3), (2,2), (2,3), (3,3)]


Практический пример — перебор всех пар:

```python
points = [(0, 0), (3, 4), (6, 8)]

for a, b in combinations(points, 2):
    dist = ((a[0]-b[0])**2 + (a[1]-b[1])**2) ** 0.5
    print(a, b, round(dist, 2))

Это чище и быстрее, чем двойной цикл с if i < j.

Перебор всех подмножеств

Классическая идиома, которую стоит запомнить:

from itertools import chain, combinations def powerset(items): return chain.from_iterable( combinations(items, r) for r in range(len(items) + 1) ) print(list(powerset([1, 2, 3]))) # [(), (1,), (2,), (3,), (1,2), (1,3), (2,3), (1,2,3)]

accumulate: накопленные значения

Префиксные суммы одной строкой:

from itertools import accumulate import operator list(accumulate([1, 2, 3, 4])) # [1, 3, 6, 10] list(accumulate([1, 2, 3, 4], operator.mul)) # [1, 2, 6, 24] — произведения list(accumulate([3, 1, 4, 1, 5], max)) # [3, 3, 4, 4, 5] — префиксный максимум list(accumulate([1, 2, 3], initial=0)) # [0, 1, 3, 6] — Python 3.8+

Параметр initial=0 особенно полезен для префиксных сумм: он даёт привычный массив длины n+1, где prefix[i] — сумма первых i элементов.

nums = [3, 1, 4, 1, 5] prefix = list(accumulate(nums, initial=0)) def range_sum(l: int, r: int) -> int: return prefix[r + 1] - prefix[l] print(range_sum(1, 3)) # 6

groupby: группировка подряд идущих

Самая недопонятая функция модуля. Она группирует только соседние одинаковые элементы, а не все одинаковые в последовательности.

from itertools import groupby data = [1, 1, 2, 2, 2, 1, 1] for key, group in groupby(data): print(key, list(group)) # 1 [1, 1] # 2 [2, 2, 2] # 1 [1, 1] ← единицы разбились на две группы!

Чтобы сгруппировать все одинаковые, данные нужно предварительно отсортировать по тому же ключу:

words = ['кот', 'дом', 'кит', 'дым'] for key, group in groupby(sorted(words, key=len), key=len): print(key, list(group)) # 3 ['дом', 'дым', 'кит', 'кот']

Зато поведение «только соседние» полезно само по себе — например, для сжатия последовательностей:

def rle_encode(s: str) -> str: """Кодирование длин серий: aaabbc → a3b2c1""" return ''.join(f'{ch}{len(list(g))}' for ch, g in groupby(s)) print(rle_encode('aaabbc')) # a3b2c1

Работа с последовательностями

chain: склейка

from itertools import chain list(chain([1, 2], [3, 4], [5])) # [1, 2, 3, 4, 5] # развернуть список списков nested = [[1, 2], [3, 4], [5, 6]] list(chain.from_iterable(nested)) # [1, 2, 3, 4, 5, 6]

chain.from_iterable — самый быстрый способ уплощить список списков, быстрее вложенного генератора.

islice: срез для итератора

Обычный срез [1:5] не работает с итераторами и генераторами. islice решает эту проблему:

from itertools import islice, count # бесконечный счётчик first_five = list(islice(count(10), 5)) # [10, 11, 12, 13, 14] # первые N строк файла без чтения всего файла with open('big.txt') as f: for line in islice(f, 10): print(line)

Бесконечные итераторы

from itertools import count, cycle, repeat count(10) # 10, 11, 12, ... count(0, 0.5) # 0, 0.5, 1.0, ... cycle('abc') # a, b, c, a, b, c, ... repeat('x', 3) # x, x, x

Обязательно ограничивайте их через islice, zip или break — иначе бесконечный цикл.

# нумерация с чередованием for i, color in zip(count(1), cycle(['красный', 'синий'])): if i > 4: break print(i, color)

pairwise: соседние пары

Появилась в Python 3.10, до этого её писали вручную:

from itertools import pairwise list(pairwise([1, 2, 3, 4])) # [(1,2), (2,3), (3,4)] # проверка возрастания nums = [1, 3, 5, 7] print(all(a < b for a, b in pairwise(nums))) # True

Фильтрация

from itertools import takewhile, dropwhile, compress, filterfalse nums = [1, 2, 3, 10, 4, 5] list(takewhile(lambda x: x < 5, nums)) # [1, 2, 3] — до первого несоответствия list(dropwhile(lambda x: x < 5, nums)) # [10, 4, 5] — после первого несоответствия list(compress('абвгд', [1, 0, 1, 0, 1])) # ['а', 'в', 'д'] — по маске list(filterfalse(lambda x: x % 2, nums)) # [2, 10, 4] — обратный filter

takewhile останавливается на первом несоответствии и не проверяет остальные — в отличие от filter, который проходит всю последовательность.

Практический пример: решение задачи перебором

Когда n маленькое, itertools заменяет ручной бэктрекинг:

from itertools import combinations def subset_sum(nums: list[int], target: int): """Найти подмножество с заданной суммой.""" for size in range(1, len(nums) + 1): for subset in combinations(nums, size): if sum(subset) == target: return list(subset) return None print(subset_sum([3, 34, 4, 12, 5, 2], 9)) # [4, 5]

Решение за O(2ⁿ), зато написано за минуту. Для n ≤ 20 этого часто достаточно.

Главное преимущество: экономия памяти

from itertools import permutations import sys # список всех перестановок 10 элементов — сотни мегабайт # perms = list(permutations(range(10))) # итератор — константная память for perm in permutations(range(10)): if perm[0] == 9: break # прервались, не построив весь список

Именно поэтому все функции возвращают итераторы. Оборачивать в list() стоит только тогда, когда результат действительно нужен целиком.

Частые ошибки

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

Повторный проход по итератору. Итератор одноразовый:

perms = permutations([1, 2, 3]) print(len(list(perms))) # 6 print(len(list(perms))) # 0 — уже исчерпан!

Использование группы после перехода к следующей. В groupby объект группы становится недействительным, как только вы перешли к следующей. Сохраняйте через list(group) сразу.

Бесконечный итератор без ограничения. for x in count(): ... без break повесит программу.

list() на больших перестановках. list(permutations(range(12))) попытается выделить память под 479 миллионов кортежей.

Что запомнить

  • Все функции возвращают итераторы — память расходуется по одному элементу.
  • product заменяет вложенные циклы, в том числе с неизвестной заранее глубиной.
  • combinations для пар и подмножеств, permutations для порядка.
  • accumulate(nums, initial=0) даёт готовые префиксные суммы.
  • groupby группирует только соседние элементы — сортируйте заранее.
  • Итератор одноразовый: повторный проход даст пустоту.

Задачи по теме