
itertools.groupby 返回的分组迭代器与原始可迭代对象共享底层数据流,一旦 groupby 对象被遍历或转换为字典,其内部指针前移,导致后续对分组子迭代器的消费返回空结果。
`itertools.groupby` 返回的分组迭代器与原始可迭代对象共享底层数据流,一旦 `groupby` 对象被遍历或转换为字典,其内部指针前移,导致后续对分组子迭代器的消费返回空结果。
itertools.groupby 是一个高效但“脆弱”的工具——它不缓存数据,而是基于输入序列的相邻元素分组,并为每个分组返回一个惰性、一次性迭代器(itertools._grouper)。关键在于:所有这些子迭代器与原始 groupby 对象共享同一个底层迭代器。这意味着,一旦 groupby 对象本身被完全消耗(例如在 dict() 构造过程中),其内部状态已推进至末尾,所有之前生成但尚未消费的子迭代器将因底层数据流“已过期”而返回空结果。
在你的代码片段 A 中:
grouped = dict(groupby(raw, key=lambda x: x["age"]))
dict() 构造函数会立即遍历整个 groupby 对象以提取 (key, grouper) 键值对。这个过程强制 groupby 内部迭代器前进到末尾,同时生成的每个 grouper 迭代器虽被保存在字典中,但其关联的底层数据流已被耗尽。因此,后续调用 list(grouped[v]) 时,得到的是空列表。
而片段 B 正确工作,是因为 for key, v_gen in grouped: 在每次循环中即时消费当前 v_gen,此时底层迭代器尚未推进到下一组,数据仍可用。
✅ 正确做法:立即将每个 grouper 转为具体容器(如 list)
from itertools import groupby
raw = [
{"name": "A", "age": 20},
{"name": "A", "age": 20},
{"name": "A", "age": 21},
{"name": "A", "age": 21},
]
# ✅ 安全:分组时立即转为 list,脱离底层迭代器依赖
grouped = {k: list(g) for k, g in groupby(raw, key=lambda x: x["age"])}
for key in grouped:
print(grouped[key])
# 输出:
# [{'name': 'A', 'age': 20}, {'name': 'A', 'age': 20}]
# [{'name': 'A', 'age': 21}, {'name': 'A', 'age': 21}]
# ✅ 或直接遍历(无需存储全部)
for key, group_iter in groupby(raw, key=lambda x: x["age"]):
print(list(group_iter)) # 每次即时消费,安全可靠
⚠️ 重要注意事项:
- groupby 要求输入已按分组键排序,否则相同键的元素若不相邻,会被拆分为多个组;
- 不要尝试多次遍历同一个 grouper 迭代器(如 list(g); list(g)),第二次必为空;
- 若需多次使用分组结果,请务必在首次获取时就转为 list、tuple 等可重复访问的结构;
- dict(groupby(...)) 本身无错,但仅适用于你不需要再消费各组内容的场景(例如只取键名)。
掌握这一行为本质,就能避免“神秘空列表”,写出健壮的分组逻辑。











