itertools: ленивые цепочки, срезы и группировка данных
Модуль itertools собирает небольшие инструменты для ленивой обработки последовательностей. Они принимают итерируемые источники, включая списки, и возвращают итераторы, поэтому…
Модуль itertools собирает небольшие инструменты для ленивой обработки последовательностей. Они принимают итерируемые источники, включая списки, и возвращают итераторы, поэтому позволяют соединять данные, ограничивать поток и группировать элементы без промежуточных списков. Польза особенно заметна на больших файлах и вычисляемых последовательностях, но одноразовость результирующих итераторов требует аккуратно выбирать момент потребления.
Три операции для конвейера
chain(a, b) последовательно выдаёт элементы нескольких источников. islice(source, start, stop, step) лениво выбирает позиции, похожие на обычный срез, однако отрицательные границы и шаги не поддерживаются. groupby(source, key=...) собирает соседние элементы с одинаковым ключом; это не глобальная группировка как в SQL.
Группы от groupby тоже являются итераторами и разделяют общий источник. Группу следует обработать или материализовать до перехода к следующей, иначе непрочитанный хвост будет потерян. Если одинаковые ключи должны попасть в одну группу, вход обычно заранее сортируют по той же функции ключа.
Рабочий пример: объединение и группировка событий
from itertools import chain, groupby, islice
morning = [('build', 3), ('build', 2)]
evening = [('test', 4), ('test', 1), ('upload', 2)]
window = islice(chain(morning, evening), 1, 5)
for kind, events in groupby(window, key=lambda item: item[0]):
duration = sum(item[1] for item in events)
print(kind, duration)Ожидаемый результат:
build 2
test 5
upload 2chain не копирует два журнала. islice пропускает нулевой элемент и выдаёт позиции с первой по четвёртую, а groupby объединяет соседние записи по типу. В этом наборе одинаковые типы уже расположены рядом.
Несоседние ключи и бесконечные потоки
Распространённая логическая ошибка — применить groupby к ['a', 'b', 'a'] и ожидать одну группу a. Получатся три группы, потому что смена ключа завершает текущую. Выведите последовательность ключей перед группировкой; если одинаковые значения разбросаны, отсортируйте конечные данные или выберите словарь накопителей, когда исходный порядок нельзя менять.
Опасная ситуация возникает с бесконечными count() или cycle(): вызов list(count()) никогда не завершится и будет расходовать память. Ограничьте поток через islice или условие остановки потребителя. При неожиданно пустом результате уточните, не был ли итератор ранее прочитан для отладки: print(list(stream)) полностью его исчерпывает.
Тренировка: окно и группировка
Есть страницы [['a', 'b'], ['c'], [], ['d', 'e']]. Через chain.from_iterable создайте единый поток, а через islice возьмите элементы с индексами от 1 до 3 включительно. Ожидаемый список — ['b', 'c', 'd']. Затем сгруппируйте строки ['ant', 'apple', 'boat', 'book'] по первой букве и выведите букву с количеством слов.
Самостоятельно поменяйте порядок на ['ant', 'boat', 'apple'], предскажите группы и подтвердите результат. После этого отсортируйте по первой букве и сравните. Проверьте ленивость, заменив страницы генератором с диагностическим print: при islice(..., 0, 2) поздние страницы не должны читаться без необходимости.
Ограничение ленивого конвейера
itertools строит одноразовые ленивые конвейеры. chain соединяет источники, islice задаёт конечное окно, а groupby объединяет только соседние равные ключи. Ограничение должно стоять до полной материализации бесконечного потока. При отладке учитывайте, что просмотр итератора меняет его состояние.
itertools без материализации
Почему islice не принимает отрицательный индекс?
Ленивый источник может не иметь длины и конца. Чтобы взять элемент «с конца», пришлось бы сначала прочитать весь поток, что противоречит модели последовательного обхода.
Нужно ли всегда сортировать перед groupby?
Нет, если нужны серии соседних событий, например непрерывные статусы. Сортировка нужна только тогда, когда задача требует свести все одинаковые ключи вместе.
Возвращает ли chain новый список?
Нет, он возвращает итератор и запрашивает элементы источников по мере надобности. Список появится лишь при явном list(...) или другом потребителе, который сохраняет всю выдачу.