1. Введение: Проблема избыточной вложенности и алгоритмической неэффективности

В профессиональной разработке на Python одной из наиболее распространенных проблем, с которыми сталкиваются специалисты среднего уровня (Middle), является написание императивного кода в стиле языков C или Pascal. Это часто приводит к появлению так называемого анти-паттерна «Arrowhead» (наконечник стрелы) — структуры с множественной вложенностью циклов и условных операторов.

Рассмотрим типичный пример задачи: необходимо обработать иерархическую структуру данных (например, отчетность организации), отфильтровать определенные значения и собрать их в плоский список для дальнейшего экспорта.

Пример неоптимальной реализации (Naive Implementation):

departments = [...]  # Список отделов
result_data = []

# Уровень вложенности 1
for department in departments:
    # Уровень вложенности 2
    for team in department.get('teams', []):
        # Уровень вложенности 3
        for employee in team.get('employees', []):
            # Уровень вложенности 4
            if employee['status'] == 'active' and employee['kpi'] > 80:
                # Аллокация памяти и добавление элемента
                result_data.append({
                    'dept_id': department['id'],
                    'emp_name': employee['name']
                })

Данный подход, будучи синтаксически корректным, обладает рядом критических недостатков с точки зрения инженерии программного обеспечения:

  1. Высокая цикломатическая сложность: Глубокая вложенность (в примере — 4 уровня) существенно затрудняет чтение и поддержку кода. Визуально код смещается вправо, что снижает его восприятие («когнитивная нагрузка» на разработчика возрастает).

  2. Неэффективное использование оперативной памяти: Использование append в цикле подразумевает создание списка, который полностью загружается в оперативную память (RAM). При обработке больших массивов данных (Big Data) это может привести к исчерпанию ресурсов и аварийному завершению процесса (OOM Kill).

  3. Низкая производительность интерпретатора: Циклы for в чистом Python выполняются интерпретатором, что накладывает накладные расходы на каждую итерацию. В отличие от скомпилированного кода, здесь происходят постоянные проверки типов и вызовы методов объектов.

Решение: Парадигма ленивых вычислений

Python спроектирован как язык, глубоко интегрированный с концепцией итераторов. Стандартная библиотека содержит модуль itertools, который предоставляет набор высокоэффективных инструментов для создания итераторов, вдохновленных функциональными языками программирования (Haskell, SML).

Ключевые преимущества перехода на itertools:

  • Memory Efficiency (Эффективность памяти): Итераторы вычисляют значения «по требованию» (lazy evaluation), не храня весь массив данных в памяти.

  • Performance (Производительность): Функции модуля реализованы на языке C, что обеспечивает высокую скорость выполнения операций, недостижимую для стандартных циклов Python.

  • Composability (Компонуемость): Возможность создавать сложные конвейеры обработки данных из простых, атомарных блоков.

В данной статье мы рассмотрим, как использование itertools позволяет рефакторить императивный код, повышая его производительность и читаемость, и перейдем от написания «велосипедов» к использованию промышленных стандартов разработки.

2. Декартово произведение как альтернатива вложенности: itertools.product

С точки зрения теории множеств, вложенные циклы часто реализуют операцию Декартова произведения (Cartesian product). Это процесс создания множества всех возможных пар (или кортежей), где первый элемент принадлежит одному множеству, а второй — другому.

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

Пример 1: Генерация товарных позиций (SKU)

Предположим, мы разрабатываем модуль для E-commerce системы. Нам необходимо сгенерировать все возможные вариации товара на основе его атрибутов (например, цвета и размера).

Императивная реализация (циклы):

colors = ['Red', 'Black', 'White']
sizes = ['S', 'M', 'L']
materials = ['Cotton', 'Polyester']

combinations = []

# Трехуровневая вложенность
for color in colors:
    for size in sizes:
        for material in materials:
            combinations.append((color, size, material))

Такой код тяжело читать, и, что более критично, он не является гибким. Если добавится четвертый атрибут (например, «Пол»), придется переписывать структуру кода, добавляя новый уровень отступа.

Реализация через itertools.product:

Функция product принимает произвольное количество итерируемых объектов и возвращает итератор кортежей.

import itertools

colors = ['Red', 'Black', 'White']
sizes = ['S', 'M', 'L']
materials = ['Cotton', 'Polyester']

# Эквивалент вложенных циклов в одну строку
sku_iterator = itertools.product(colors, sizes, materials)

for item in sku_iterator:
    print(item) 
    # Output: ('Red', 'S', 'Cotton'), ('Red', 'S', 'Polyester')...

Этот подход делает код «плоским» (flat), что соответствует дзену Python («Flat is better than nested»).

Пример 2: Работа с переменным числом измерений

Главное преимущество product раскрывается в ситуациях, когда глубина вложенности заранее неизвестна или определяется динамически во время выполнения программы.

Рассмотрим задачу генерации тестовых данных (Fuzzing) или перебора пространства состояний, где нам нужно получить все возможные последовательности определенной длины.

Задача: Сгенерировать все возможные PIN-коды длиной 4 цифры.

Реализовать это через циклы for затруднительно, если длина PIN-кода является переменной N (придется использовать рекурсию). В itertools.product для этого предусмотрен аргумент repeat.

# Генерация пространства вариантов для PIN-кода длиной 4
digits = '0123456789'

# repeat=4 заменяет 4 вложенных цикла
pin_space = itertools.product(digits, repeat=4)

# Пример итерации
# ('0', '0', '0', '0')
# ('0', '0', '0', '1')
# ...
# ('9', '9', '9', '9')

Техническое резюме:
Использование itertools.product позволяет перевести алгоритмическую сложность O(N^k) из визуальной структуры кода во внутреннюю реализацию на языке C. Это не меняет вычислительную сложность алгоритма, но радикально повышает поддерживаемость кода и скорость работы интерпретатора за счет отказа от выполнения байт-кода Python на каждой итерации внутренних циклов.

3. Пакетная обработка данных: itertools.batched (Python 3.12+)

В задачах интеграции информационных систем, ETL-процессах (Extract, Transform, Load) и при работе с базами данных часто возникает необходимость обработки больших массивов данных частями (батчами, чанками).

Типичные сценарии использования:

  1. Bulk Insert в БД: Вставка 1000 записей одним SQL-запросом эффективнее, чем 1000 отдельных запросов.

  2. API Rate Limiting: Внешние сервисы часто ограничивают количество элементов в одном запросе (например, не более 100 ID пользователей за раз).

  3. Ограничение памяти: Построчное чтение огромного CSV-файла и обработка его блоками, чтобы не загружать весь файл в RAM.

Проблематика до Python 3.12

До выхода версии 3.12 в стандартной библиотеке отсутствовал прямой инструмент для разбиения итератора на пакеты фиксированного размера. Разработчикам приходилось прибегать к написанию собственных утилит или использованию срезов списков.

Устаревший подход (Slicing):

data = [1, 2, 3, ..., 10000]
batch_size = 100

# Проблема: Требует загрузки всех данных в список (в память).
# Не работает с генераторами и потоковыми данными.
batches = [data[i:i + batch_size] for i in range(0, len(data), batch_size)]

Этот метод неэффективен для «ленивых» последовательностей (например, чтение строк из сетевого сокета), так как требует предварительного вычисления длины объекта и наличия произвольного доступа к элементам по индексу.

Современное решение: itertools.batched

Начиная с Python 3.12, модуль itertools предоставляет функцию batched, которая корректно работает с любыми итерируемыми объектами, включая генераторы, не загружая лишние данные в память.

Пример реализации:

import itertools
import time

def process_api_request(ids_batch):
    """Эмуляция отправки запроса во внешний сервис."""
    print(f"Отправка пакета из {len(ids_batch)} элементов: {ids_batch}")
    # Реальный сетевой вызов был бы здесь

# Имитация потока данных (например, выборка из БД курсором)
# generator_data не занимает память целиком
generator_data = (x for x in range(1, 1005)) 

BATCH_SIZE = 100

# Элегантная итерация пакетами
for batch in itertools.batched(generator_data, BATCH_SIZE):
    process_api_request(batch)
    
# Результат работы:
# Отправка пакета из 100 элементов: (1, 2, ..., 100)
# ...
# Отправка пакета из 4 элементов: (1001, 1002, 1003, 1004)

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

Решение для версий Python < 3.12

В корпоративном секторе часто используются более старые версии интерпретатора (3.8–3.11). В таких случаях рекомендуется не писать собственные реализации («велосипеды»), а использовать проверенную библиотеку more-itertools, которая является де-факто стандартом для расширенных операций над итераторами.

# Для старых версий Python
from more_itertools import chunked

# chunked возвращает списки, chunked_even - кортежи
for batch in chunked(generator_data, 100):
    process_api_request(batch)

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

4. Эффективная конкатенация последовательностей: itertools.chain

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

Проблема оператора сложения (+)

Наиболее очевидным способом объединения списков в Python является использование оператора +.

admins = [user1, user2, ...]    # 10 000 объектов
moderators = [user3, user4, ...] # 50 000 объектов
users = [user5, user6, ...]      # 1 000 000 объектов

# Аллокация памяти под новый список размером 1 060 000 элементов
all_people = admins + moderators + users 

for person in all_people:
    process(person)

С точки зрения алгоритмов, эта операция является жадной (eager). Интерпретатор выполняет следующие действия:

  1. Выделяет непрерывный блок памяти под новый объект списка.

  2. Копирует ссылки на элементы из первого списка.

  3. Копирует ссылки из второго и третьего списков.

Это приводит к пиковому потреблению памяти, равному сумме объемов исходных списков плюс размер нового списка. Для больших данных (Big Data) или высоконагруженных веб-серверов это недопустимое расточительство ресурсов.

Решение: Виртуальная склейка через chain

Функция itertools.chain реализует паттерн проектирования «Итератор». Она принимает на вход произвольное количество итерируемых объектов и создает «виртуальную» последовательность.

import itertools

# Память не выделяется под новый список.
# Создается только легковесный объект итератора.
people_iterator = itertools.chain(admins, moderators, users)

for person in people_iterator:
    # Итератор просто переключается на следующий список,
    # когда заканчивается предыдущий.
    process(person)

В данном случае накладные расходы памяти составляют O(1), то есть являются константными и не зависят от объема данных. itertools.chain не копирует данные, а лишь последовательно делегирует вызов next() соответствующему исходному итератору.

Продвинутый сценарий: chain.from_iterable

Частый сценарий при обработке данных — необходимость «выпрямить» (flatten) двумерную структуру. Например, результат пагинации API часто представляет собой список списков (страниц с результатами).

Вместо использования двойного цикла for или списковых включений (list comprehensions), которые загружают данные в память, следует использовать метод класса from_iterable.

# Имитация ответа от базы данных или API (список страниц)
paginated_response = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# Неэффективно (создает новый список):
# flat_list = [item for page in paginated_response for item in page]

# Профессионально (ленивый итератор):
flat_iterator = itertools.chain.from_iterable(paginated_response)

for item in flat_iterator:
    print(item) # 1, 2, 3, 4...

Резюме:
Использование chain и chain.from_iterable является стандартом при работе с потоковыми данными. Это позволяет обрабатывать последовательности теоретически бесконечной длины, ограниченные лишь временем выполнения, но не объемом оперативной памяти.

5. Агрегация данных по ключу: itertools.groupby

В разработке бэкенд-систем одной из типовых задач является группировка списка объектов по определенному признаку. Это прямой аналог оператора GROUP BY в языке SQL. Примеры включают формирование отчета по транзакциям за день, группировку сотрудников по отделам или кластеризацию логов по уровню ошибки.

Императивный подход (Словарь списков)

Традиционное решение на Python подразумевает создание промежуточного словаря, где ключом является признак группировки, а значением — список объектов.

transactions = [
    {'date': '2023-10-01', 'amount': 100, 'id': 1},
    {'date': '2023-10-02', 'amount': 200, 'id': 2},
    {'date': '2023-10-01', 'amount': 150, 'id': 3},
]

grouped_data = {}

for tx in transactions:
    date = tx['date']
    if date not in grouped_data:
        grouped_data[date] = []
    grouped_data[date].append(tx)

# Результат: словарь с двумя ключами и списками транзакций

Хотя этот код работает корректно, он требует полной материализации данных в памяти (создания объемного словаря) и написания шаблонного кода (boilerplate) для инициализации списков.

Функциональный подход: itertools.groupby

Функция groupby позволяет выполнить эту операцию элегантно, возвращая итератор пар (ключ, группа).

Однако здесь существует критический архитектурный нюанс, отличающий реализацию в Python от SQL. В базах данных GROUP BY часто выполняет неявную сортировку или хеширование всего набора данных. Функция itertools.groupby работает иначе: она сканирует последовательность линейно и создает новую группу каждый раз, когда значение ключа меняется.

Важное правило: Входные данные для groupby обязательно должны быть отсортированы по ключу группировки.

Корректная реализация:

import itertools
from operator import itemgetter

transactions = [
    {'date': '2023-10-01', 'amount': 100, 'id': 1},
    {'date': '2023-10-02', 'amount': 200, 'id': 2},
    {'date': '2023-10-01', 'amount': 150, 'id': 3},
]

# 1. Обязательная сортировка данных
# Используем itemgetter для извлечения ключа (быстрее, чем lambda)
key_func = itemgetter('date')
transactions.sort(key=key_func)

# 2. Группировка
# groupby возвращает итератор, поэтому используем цикл
for date, group_iterator in itertools.groupby(transactions, key=key_func):
    
    # group_iterator — это тоже итератор!
    # Чтобы работать с данными, их часто нужно превратить в список
    # или агрегировать (например, посчитать сумму)
    
    total_amount = sum(item['amount'] for item in group_iterator)
    print(f"Дата: {date}, Сумма: {total_amount}")

# Output:
# Дата: 2023-10-01, Сумма: 250
# Дата: 2023-10-02, Сумма: 200

Если пропустить этап сортировки, groupby воспримет записи {'date': '2023-10-01'...} в начале и в конце списка как разные группы, что приведет к логической ошибке в отчете.

RLE-алгоритм (Run-Length Encoding)

Благодаря своей механике (объединение идущих подряд одинаковых элементов), groupby является идеальным инструментом для реализации алгоритмов сжатия данных.

Пример сжатия строки:

raw_data = "AAAABBBCCDAA"
compressed = []

for char, group in itertools.groupby(raw_data):
    # group преобразуем в список, чтобы узнать его длину
    count = len(list(group))
    compressed.append((char, count))

print(compressed)
# [('A', 4), ('B', 3), ('C', 2), ('D', 1), ('A', 2)]

Техническое резюме:
Используйте itertools.groupby для обработки потоковых данных или когда вы хотите избежать создания громоздких словарей. Но всегда помните о необходимости предварительной сортировки (list.sort() или sorted()) по целевому критерию.

6. Комбинаторика и алгоритмические задачи: combinations и permutations

В процессе технических интервью на позиции уровня Middle+ часто встречаются задачи, требующие перебора вариантов: поиск пар чисел с определенной суммой, генерация анаграмм или планирование турнирных таблиц.

Попытка реализовать математические алгоритмы перестановок и сочетаний вручную (через вложенные циклы или рекурсию) часто приводит к ошибкам типа «Off-by-one error» (ошибка на единицу) и усложняет чтение кода. Модуль itertools предоставляет оптимизированные реализации этих алгоритмов на C.

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

  1. Combinations (Сочетания): Порядок не важен ({A, B} эквивалентно {B, A}).

  2. Permutations (Перестановки): Порядок важен ({A, B} не равно {B, A}).

Задача 1: Поиск пар (Сочетания)

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

Императивный подход (Вложенные циклы):
Чтобы избежать дубликатов (матч "Команда А vs Команда Б" и "Команда Б vs Команда А" — это одна игра) и матчей с самими собой, во внутреннем цикле приходится манипулировать индексами (j = i + 1).

teams = ['Team A', 'Team B', 'Team C', 'Team D']
matches = []

for i in range(len(teams)):
    # Начинаем с i + 1, чтобы не повторяться
    for j in range(i + 1, len(teams)):
        matches.append((teams[i], teams[j]))

# Результат: [('Team A', 'Team B'), ('Team A', 'Team C')...]

В этом коде легко ошибиться с диапазонами range.

Решение через itertools.combinations:
Функция принимает итерируемый объект и длину выходного кортежа r.

import itertools

teams = ['Team A', 'Team B', 'Team C', 'Team D']

# r=2 означает, что мы ищем пары
match_iterator = itertools.combinations(teams, 2)

for match in match_iterator:
    print(match)
    # Гарантированно уникальные пары без учета порядка

Задача 2: Варианты размещения (Перестановки)

Другой класс задач связан с упорядочиванием. Пример: у нас есть 3 призовых места и 3 участника. Сколькими способами они могут распределить золотую, серебряную и бронзовую медали? Здесь вариант "Иванов — 1, Петров — 2" отличается от "Петров — 1, Иванов — 2".

Императивный подход:
Требует проверки условия if i != j, чтобы исключить повторное использование одного и того же элемента в рамках одной выборки. При увеличении длины выборки код становится экспоненциально сложным.

Решение через itertools.permutations:

participants = ['Ivanov', 'Petrov', 'Sidorov']

# Генерируем все возможные варианты распределения 3 мест
# Если второй аргумент не указан, он равен длине списка
awards_variants = itertools.permutations(participants, 3)

print(list(awards_variants))
# [
#   ('Ivanov', 'Petrov', 'Sidorov'), 
#   ('Ivanov', 'Sidorov', 'Petrov'),
#   ('Petrov', 'Ivanov', 'Sidorov'),
#   ...
# ]

Вычислительная сложность и память

Важно понимать, что количество комбинаторных вариантов растет факториально.

  • Для списка из 10 элементов количество перестановок (10!) составляет 3 628 800.

  • Для 15 элементов — уже более 1 триллиона.

Преимущество itertools здесь критично: функции возвращают итераторы, а не готовые списки.
Вызов itertools.permutations(range(20)) выполнится мгновенно и займет несколько байт памяти. Попытка сделать list() от этого результата немедленно приведет к переполнению оперативной памяти (Memory Error).

Использование этих инструментов показывает интервьюеру, что вы понимаете разницу между «ленивыми» вычислениями и материализацией данных, а также знаете стандартную библиотеку, экономя время на написание boilerplate-кода.

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

В стандартной библиотеке itertools существует класс функций, генерирующих потенциально бесконечные последовательности. Их главное предназначение — избавить разработчика от необходимости вручную управлять состоянием переменных внутри циклов while True и реализовывать сложную логику сброса индексов.

Генерация арифметических последовательностей: count

Частая задача: необходимо пронумеровать элементы потока данных, начиная с определенного значения, или сгенерировать уникальные идентификаторы с нестандартным шагом.

Императивный подход:
Разработчик вынужден инициализировать переменную-счетчик перед циклом и вручную инкрементировать её на каждой итерации.

# Ручное управление состоянием
current_id = 1000
step = 5

for data in data_stream:
    process(data, id=current_id)
    current_id += step

Решение через itertools.count:
Функция count(start, step) возвращает итератор, который генерирует числа бесконечно. Это идеальная пара для функции zip.

import itertools

data_stream = ['Event A', 'Event B', 'Event C']

# Автоматическая генерация ID
# Аналог enumerate, но с полным контролем над стартом и шагом
id_generator = itertools.count(start=1000, step=5)

for event, event_id in zip(data_stream, id_generator):
    print(f"ID: {event_id}, Data: {event}")

# Output:
# ID: 1000, Data: Event A
# ID: 1005, Data: Event B
# ...

Циклическое переключение (Round-Robin): cycle

Классический паттерн распределения нагрузки или задач — Round-Robin. Например, у нас есть 3 рабочих сервера (воркера), и нам нужно распределять входящие задачи между ними по очереди: 1 -> 2 -> 3 -> 1 -> 2...

Императивный подход:
Обычно реализуется через деление по модулю (%). Это работает, но добавляет «синтаксический шум» и требует знания длины списка.

workers = ['Worker-1', 'Worker-2', 'Worker-3']
tasks = range(10)

for i, task in enumerate(tasks):
    # Вычисление индекса через модуль
    worker = workers[i % len(workers)]
    assign_task(worker, task)

Решение через itertools.cycle:
Функция берет последовательность, запоминает её и бесконечно повторяет по кругу.

workers = ['Worker-1', 'Worker-2', 'Worker-3']
tasks = range(10)

# Создаем бесконечный пул воркеров
worker_pool = itertools.cycle(workers)

for task in tasks:
    # Просто берем следующего
    current_worker = next(worker_pool)
    print(f"Задача {task} -> {current_worker}")

Это решение не зависит от количества задач и не требует индексной арифметики. Код становится декларативным: «Для каждой задачи дай следующего воркера из пула».

Константные значения: repeat

Функция repeat(object, times) возвращает один и тот же объект заданное количество раз (или бесконечно).

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

Пример: Возведение списка чисел в квадрат

numbers = [1, 2, 3, 4, 5]

# map ожидает, что количество аргументов совпадает.
# Мы передаем бесконечный поток двоек в качестве второго аргумента для pow(x, y)
squares = map(pow, numbers, itertools.repeat(2))

print(list(squares)) 
# [1, 4, 9, 16, 25]

Это эффективнее по памяти, чем создание списка [2, 2, 2, 2, 2], так как repeat не занимает память под дубликаты значений.

Предостережение:
Поскольку итераторы count, cycle и repeat (без аргумента times) являются бесконечными, их нельзя использовать в цикле for или функциях типа list() без ограничивающего условия (например, zip с конечным списком или itertools.islice). В противном случае программа уйдет в бесконечный цикл и зависнет.

8. Заключение: Ленивые вычисления спасут мир

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

Подводя итог, выделим три фундаментальные причины внедрять эти инструменты в ваши проекты:

  1. Эффективность ресурсов (Resource Efficiency):
    В эпоху облачных вычислений и контейнеризации (Docker/Kubernetes), где объем оперативной памяти жестко лимитирован, «ленивые» вычисления (lazy evaluation) становятся стандартом. Итераторы не хранят данные, они генерируют их по требованию. Это позволяет обрабатывать гигабайтные файлы логов или миллионные выборки из БД на микросервисе с 512 МБ RAM, сохраняя сложность по памяти O(1).

  2. Снижение когнитивной нагрузки (Readability):
    Стандартная библиотека Python — это общий словарь для всех разработчиков. Конструкция itertools.product мгновенно считывается коллегой как «декартово произведение», в то время как каскад из трех циклов for требует внимательного анализа тела цикла, чтобы понять замысел автора. Использование стандартных примитивов делает код самодокументируемым.

  3. Надежность и скорость (Performance & Stability):
    Функции itertools написаны на языке C, отлажены и оптимизированы десятилетиями использования в миллионах проектов. Пытаясь написать собственный алгоритм группировки или комбинаторики, вы рискуете допустить ошибку (например, в граничных условиях) и почти наверняка проиграете в производительности нативному коду.

Рекомендация:
В следующий раз, когда ваша рука потянется написать вложенный цикл или создать пустой список result = [] перед итерацией, сделайте паузу. Откройте документацию itertools (или эту статью). Скорее всего, Python уже решил вашу задачу элегантно, быстро и эффективно.

Используйте «батарейки», которые идут в комплекте с языком. Это отличает профессионала от любителя.


? Домашнее задание: Закрепляем на практике

Вы изучили теорию, теперь время проверить навыки на реальных кейсах. Я подготовил 5 задач, решение которых с помощью циклов заняло бы десятки строк, а с itertools — одну-две.

? Нажмите, чтобы открыть список задач (5 шт.)

Задача 1: Взломщик сейфа
У вас есть набор символов symbols = ['A', 'B', 'C']. Сгенерируйте все возможные пароли длиной 3 символа. Порядок важен, символы могут повторяться (например, 'AAA').
(Подсказка: используйте product)

Задача 2: Командные соревнования
Есть список участников: players = ['Ivan', 'Maria', 'Bob', 'Alice']. Нужно разбить их на все возможные пары для игры друг против друга. Матч "Ivan vs Maria" и "Maria vs Ivan" — это одна и та же игра.
(Подсказка: порядок в паре не важен, используйте combinations)

Задача 3: Вечный светофор
Создайте итератор, который при каждом вызове next() выдает цвета в бесконечном цикле: 'Red', 'Yellow', 'Green', 'Red', 'Yellow', ...
(Подсказка: используйте cycle)

Задача 4: Объединение квартальных отчетов
У вас есть три списка продаж: sales_jan, sales_feb, sales_mar. Проитерируйтесь по всем продажам квартала в одном цикле, не создавая новый общий список в памяти (без +).
(Подсказка: используйте chain)

Задача 5: RLE-сжатие (Собеседование в Google)
Дана строка s = "AAAABBBCCDAA". Используя itertools, превратите её в список кортежей вида [('A', 4), ('B', 3), ('C', 2), ('D', 1), ('A', 2)].
(Подсказка: используйте groupby, и не забудьте, что len() не работает с итераторами напрямую)


?️‍♂️ Ответы для самопроверки:

  1. list(itertools.product(symbols, repeat=3))

  2. list(itertools.combinations(players, 2))

  3. colors = itertools.cycle(['Red', 'Yellow', 'Green'])

  4. for sale in itertools.chain(sales_jan, sales_feb, sales_mar): ...

  5. [(char, len(list(group))) for char, group in itertools.groupby(s)]

Анонсы новых статей, полезные материалы, а так же если в процессе у вас возникнут сложности, обсудить их или задать вопрос по этой статье можно в моём Telegram-сообществе. Смело заходите, если что-то пойдет не так, — постараемся разобраться вместе.

Уверен, у вас все получится. Вперед, к экспериментам

Комментарии (4)


  1. outlingo
    24.11.2025 08:17

    У вас GPT поломалась. В качестве первого примера была взята задача на обход иерархической структуры, после чего зачем-то начали показываться комбинаторные примеры и простые итераторы, которые к исходной задаче имеют отношение и применимость чуть меньше чем никакие.


    1. enamored_poc Автор
      24.11.2025 08:17

      Спасибо за внимательность! Вы абсолютно правы насчет разницы алгоритмов.

      Пример во введении я привел как самую узнаваемую визуализацию анти-паттерна "Arrowhead" (бесконечные отступы вправо), который знаком всем. А itertools.product привел первым пунктом как самый простой способ избавиться от вложенности в комбинаторных задачах, так как они нагляднее для старта.

      Согласен, что переход получился резким. Задачу из введения по-хорошему нужно решать через генераторы или itertools.chain.from_iterable, "выпрямляя" списки команд и сотрудников. Рад, что статью читают вдумчиво, учту этот момент в будущих материалах!


  1. cupraer
    24.11.2025 08:17

    Python спроектирован как язык, глубоко интегрированный с концепцией итераторов.

    Просто для справки: у слова «спроектирован» есть словарное значение, которым имеет смысл пользоваться, чтобы не вводить доверчивого читателя в заблуждение. Модуль itertools появился в версии 2.3. Сиречь, 12 (двенадцать) лет после создания языка.

    В питоне уже несколько лет как накостылили рекурсию с TCO. Адекватные люди, если им приходится столкнуться с питоном, пользуются ей, а не кривыми итераторами.


    1. enamored_poc Автор
      24.11.2025 08:17

      1. Спасибо за историческую справку по версии 2.3. Действительно, модуль появился тогда, но с тех пор язык эволюционировал, и в Python 3 концепция итераторов стала фундаментальной (PEP 234).

      2. Про TCO: Пожалуйста, поделитесь ссылкой на PEP или release notes CPython, где добавили нативную поддержку TCO. Насколько известно официальной документации и BDFL, в Python оптимизации хвостовой рекурсии нет, и глубокая рекурсия вызывает переполнение стека. Итераторы — единственный безопасный способ обработки больших данных.