Python groupby 分组实战:相邻片段和全局汇总是两种问题
先用一条交错记录确定需求
假设流水按到达顺序记录为 A 收到两件、A 收到三件、B 收到七件、A 又收到五件。运营想看每段连续处理量,财务想看每位客户的总量。输入完全相同,答案却不同:前者有三段,后者只有两个客户。选工具前先把这两个答案写下来,能防止函数名称替你决定业务含义。
Python 官方文档把 itertools.groupby 定义为相邻相同键的分组工具;键变化就开启新组。它不主动把远处的同键记录搬到一起。若需要排序后再分组,排序和分组应使用相同的键函数。另外,各组迭代器共享底层输入,需要长期保留内容时,应在推进外层之前读取并保存。
AI生成概念示意图,非真实界面
在同一份输入上运行三种算法
下面代码只用标准库,可保存为 Python 文件直接执行。每条记录的第二项代表件数,不是记录条数。因此用 sum 累计数值,而不是用组长度冒充业务总量。例子故意让 A 在末尾再次出现,这一行就是验证需求边界的关键;只用已经排好序的数据测试,三个写法看起来都会正确。
from itertools import groupby
from collections import defaultdict
from operator import itemgetter
rows = [('A', 2), ('A', 3), ('B', 7), ('A', 5)]
key = itemgetter(0)
adjacent = [(name, sum(n for _, n in group))
for name, group in groupby(rows, key)]
ordered = [(name, sum(n for _, n in group))
for name, group in groupby(sorted(rows, key=key), key)]
buckets = defaultdict(int)
for name, amount in rows:
buckets[name] += amount
print('adjacent:', adjacent)
print('sorted:', ordered)
print('buckets:', dict(buckets))
assert adjacent == [('A', 5), ('B', 7), ('A', 5)]
assert ordered == [('A', 10), ('B', 7)]
assert dict(buckets) == {'A': 10, 'B': 7}
outer = groupby(rows, key)
first_key, first_group = next(outer)
second_key, second_group = next(outer)
print('old group:', list(first_group))
assert list(first_group) == []
saved = [(name, list(group)) for name, group in groupby(rows, key)]
print('saved:', saved)
assert saved[0] == ('A', [('A', 2), ('A', 3)])第一行应显示 A 为五、B 为七、A 又为五,说明最后一段没有被并入开头。第二行显示 A 为十、B 为七,第三行字典给出同样总数。请先人工计算四笔输入,再对照断言;断言只能检查作者写下的预期,人工核算才能发现预期本身是否抄错。这里还保留原始 rows,让三种算法各自读取完整样本。
排序改变的是问题里的顺序
如果任务是找连续异常区间,先排序会破坏时间上的邻接关系。把两次相隔很久的 A 合并,得到的虽然是正确客户总量,却不再能回答处理中断过几次。反过来,按客户做日报时,原顺序通常无需保留,此时可以排序分组,也可以用字典累计。例子里的字典只存总数,适合不需要逐条明细的汇总。
扩展成大文件时,内存要按完整处理链计算。groupby 本身逐组消费,不代表前面的 sorted 也能流式工作;排序需要先读入这里的全部记录。字典累计保留的是每个客户的状态,客户种类越多,占用也会增长。若输入本来已按客户排列,可以边读取边汇总,但应该在数据契约里写清这个前提。
最后检查组对象有没有被提前耗尽
代码随后先取第一组,再推进到第二组,旧组打印为空。这不是记录消失,而是共享输入已经向前移动。常见错误是把所有 group 对象放进列表,等外层结束才逐一读取。最后的 saved 写法在当前轮立即转成列表,第一段 A 的两条记录才会保留下来;它保留的是分段结构,不会顺便完成全局合并。
验收时保留“同键隔开后再出现”的样本,并额外试空输入与只有一条记录的输入。
若键来自大小写转换或日期截取,排序和分组复用同一个函数,避免边界口径不同。
需要金额累计时另行约定金额类型与精度;分组正确并不保证数值计算符合账务规则。


