itertools.groupby повторил клиента: почему сначала нужен порядок ключей
В отчёте по клиентам один идентификатор появился дважды. Разработчик уже использовал groupby и ожидал одну строку на клиента. Данные пришли в порядке событий: покупка A, покупка B, ещё одна покупка A. Для itertools это три последовательные группы. Название функции похоже на операцию базы данных, но правило объединения у неё другое.
Группы соседей и общий итог
groupby сравнивает ключ текущего элемента с ключом предыдущего. При изменении ключа начинается новая группа. Он не держит словарь всех увиденных клиентов и не возвращается в старую группу. Такой механизм полезен для серий: одинаковых статусов подряд, последовательных запросов одного соединения или участков лога. Для общего итога по ключу сначала нужно расположить равные ключи рядом либо выбрать накопление в словаре.
В примере явно решаются две разные задачи. Первая часть сохраняет исходный порядок и считает серии. Вторая сортирует тот же набор по клиенту и получает общий итог по клиенту. Результаты различаются предсказуемо: у A две серии с суммами 10 и 7, но один итог 17. Это помогает выбрать операцию по смыслу отчёта, а не просто устранить повтор заголовка.
Есть и вторая ловушка: объект группы читает тот же источник, что и внешний groupby. Если сохранить групповые итераторы и продолжить внешний обход, старую группу можно потерять. Поэтому агрегат здесь вычисляется внутри текущей итерации. Когда сами записи нужны позже, преобразуйте текущую группу в list именно в этом месте и учитывайте её размер.
Сравнить два порядка входа
from itertools import groupby
from operator import itemgetter
rows = [("A", 10), ("B", 5), ("A", 7)]
key = itemgetter(0)
def summarize(source):
return [
(client, sum(amount for _, amount in group))
for client, group in groupby(source, key=key)
]
runs = summarize(rows)
totals = summarize(sorted(rows, key=key))
assert runs == [("A", 10), ("B", 5), ("A", 7)]
assert totals == [("A", 17), ("B", 5)]
assert sum(amount for _, amount in totals) == 22
print("runs:", runs)
print("totals:", totals)
Сортировать следует по тому же правилу, которое использует группировка. Если отчёт объединяет клиентов без учёта регистра, функция key должна нормализовать идентификатор и при сортировке, и при groupby. Сортировка по дате с группировкой по клиенту не гарантирует соседство: покупатели продолжат чередоваться. Такое несоответствие легко пропустить на входе, где случайно встретился только один клиент.
Выбрать алгоритм по задаче
Цена сортировки существенна для большого потока: sorted сначала потребляет весь вход и создаёт список. Если база уже выдаёт строки в ORDER BY client_id, можно группировать поток напрямую и держать только текущую группу. Если порядок не нужен, словарь с накоплением обычно проще: он хранит итог на каждый ключ и не требует сортировки всех записей. Выбирайте по доступному порядку, размеру входа и требованию к выходу.
Для поиска серий сортировать вообще нельзя: порядок событий является частью задачи. Последовательность ошибок A, успех B, ошибка A должна сохранить разрыв, иначе два отдельных сбоя превратятся в одну серию. Полезно назвать функцию по результату — runs_by_client или totals_by_client. Тогда выбранная семантика видна вызывающему коду.
Добавьте вход с повтором ключа после чужой записи, пустой набор и группу из одного элемента. Проверьте сохранение общей суммы отдельно от числа групп. Общая сумма может оставаться правильной даже в сломанном отчёте, где один клиент размножился; поэтому она не заменяет проверку ключей. Если нужен ровно один результат на клиента, проверьте также уникальность ключей на выходе.
Этот пример относится к стандартной библиотеке Python. Наличие похожего groupby в других инструментах не переносит сюда их правило работы с несортированными данными. При замене реализации фиксируйте контракт в тесте на чередующихся ключах: он объясняет намерение лучше, чем комментарий «группируем клиентов».
Продолжить практику
В «Python для профессионалов» работа с итераторами и генераторами помогает разбирать такие ленивые преобразования по шагам. Откройте программу курса и продолжите с моментом потребления источника, чтобы выбор потоковой операции опирался на порядок данных и нужный результат.
Продолжить на практике
- Python для профессионалов ↗
Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.
