groupby 是 Python 标准库 itertools 模块中的一个非常有用的函数,它的主要作用是将一个可迭代对象中的连续、相同的元素分组,并返回一个迭代器,每次迭代产生一个键和对应的分组迭代器。

核心概念:itertools.groupby
最重要的一点是:groupby 只对相邻的相同元素进行分组,如果你的数据是乱序的,必须先进行排序,否则分组结果会不正确。
函数签名
itertools.groupby(iterable, key=None)
参数
iterable: 一个可迭代对象,比如列表、元组等。key: 一个可选的函数,用于从每个元素中提取一个键,这个键将用于分组,如果未提供key函数,groupby默认直接使用元素本身作为键。
返回值
- 返回一个迭代器,这个迭代器产生的元素是
(key, group_iterator)对。key: 分组的键。group_iterator: 一个迭代器,可以遍历当前分组内的所有元素。
工作原理与使用步骤
groupby 的工作方式是“懒加载”的,它不会一次性处理所有数据,而是在迭代时逐个处理,要正确使用它,通常遵循以下步骤:
步骤 1:排序
这是最关键的一步!必须先对要分组的字段进行排序。groupby 只会“看到”连续的元素。
步骤 2:遍历
使用 for 循环来遍历 groupby 返回的迭代器。

步骤 3:处理每个分组
对于 (key, group_iterator) 对,你可以再次使用 for 循环来遍历 group_iterator,从而获取分组内的每一个元素。
示例讲解
示例 1:最基本的使用(无 key 函数)
假设我们有一个列表,想统计连续出现的数字。
from itertools import groupby
data = [1, 1, 2, 2, 2, 3, 3, 1, 1, 4]
# 1. 数据已经是有序的了,可以直接使用 groupby
# 2. 遍历 groupby 的结果
for key, group in groupby(data):
# key 是分组的元素 (1, 2, 3, 1, 4)
# group 是一个迭代器,包含了该组内的所有元素
print(f"键: {key}, 分组元素: {list(group)}")
输出:
键: 1, 分组元素: [1, 1]
键: 2, 分组元素: [2, 2, 2]
键: 3, 分组元素: [3, 3]
键: 1, 分组元素: [1, 1]
键: 4, 分组元素: [4]
注意: 为什么会有两个 1 的分组?因为它们不是连续的,这再次证明了排序的重要性。

示例 2:使用 key 函数(最常见用法)
假设我们有一个学生列表,想按班级进行分组。
from itertools import groupby
students = [
{'name': 'Alice', 'class': 'A'},
{'name': 'Bob', 'class': 'B'},
{'name': 'Charlie', 'class': 'A'},
{'name': 'David', 'class': 'B'},
{'name': 'Eve', 'class': 'A'},
{'name': 'Frank', 'class': 'C'}
]
# 1. 必须先按 'class' 字段排序!
# 我们使用 lambda 函数指定排序的键
sorted_students = sorted(students, key=lambda x: x['class'])
print("排序后的学生列表:")
print(sorted_students)
print("-" * 20)
# 2. 使用 groupby,并指定 key 函数
for class_name, group_iter in groupby(sorted_students, key=lambda x: x['class']):
print(f"班级: {class_name}")
# 3. 遍历当前班级的迭代器,获取所有学生
for student in group_iter:
print(f" - {student['name']}")
print() # 打印一个空行,使输出更清晰
输出:
排序后的学生列表:
[{'name': 'Alice', 'class': 'A'}, {'name': 'Charlie', 'class': 'A'}, {'name': 'Eve', 'class': 'A'}, {'name': 'Bob', 'class': 'B'}, {'name': 'David', 'class': 'B'}, {'name': 'Frank', 'class': 'C'}]
--------------------
班级: A
- Alice
- Charlie
- Eve
班级: B
- Bob
- David
班级: C
- Frank
示例 3:统计分组数量
一个常见的用例是计算每个分组的元素个数,我们可以使用 sum() 和生成器表达式来高效地完成。
from itertools import groupby
data = [('a', 1), ('a', 2), ('b', 3), ('b', 4), ('b', 5), ('c', 6)]
# 1. 按元组的第一个元素排序
sorted_data = sorted(data, key=lambda x: x[0])
# 2. 分组并统计
for key, group in groupby(sorted_data, key=lambda x: x[0]):
# group 是一个迭代器,('a', 1), ('a', 2)
# len(list(group)) 会消耗迭代器,所以我们需要一种不消耗它的方法
# 一个更 Pythonic 的方法是使用 sum(1 for _ in group)
count = sum(1 for _ in group)
print(f"键: {key}, 数量: {count}")
输出:
键: a, 数量: 2
键: b, 数量: 3
键: c, 数量: 1
常见误区与注意事项
误区 1:忘记排序
这是 groupby 最常见的错误。
# 错误示例
data = [2, 2, 1, 1, 3, 3]
for key, group in groupby(data):
print(key, list(group))
# 输出:
# 2 [2, 2]
# 1 [1, 1]
# 3 [3, 3]
# 这个例子看起来“碰巧”对了,但如果数据是 [2, 1, 2, 1] 呢?
data = [2, 1, 2, 1]
for key, group in groupby(data):
print(key, list(group))
# 输出:
# 2 [2]
# 1 [1]
# 2 [2]
# 1 [1]
# 分组结果完全错误!
误区 2:迭代器只能消耗一次
groupby 返回的 group_iter 是一个迭代器,它只能被遍历一次,如果你尝试遍历两次,第二次会得到空的结果。
from itertools import groupby
data = [1, 1, 2, 2, 3, 3]
sorted_data = sorted(data) # [1, 1, 2, 2, 3, 3]
for key, group_iter in groupby(sorted_data):
print(f"键: {key}")
# 第一次遍历
print("第一次遍历:", list(group_iter))
# 第二次遍历
print("第二次遍历:", list(group_iter)) # 会得到空列表 []
输出:
键: 1
第一次遍历: [1, 1]
第二次遍历: []
键: 2
第一次遍历: [2, 2]
第二次遍历: []
键: 3
第一次遍历: [3, 3]
第二次遍历: []
误区 3:key 函数的误用
key 函数是用来提取分组依据的,而不是用来过滤数据的,如果你想在 key 函数里做 if 判断来过滤掉某些元素,groupby 的行为可能会让你感到困惑。
需求: 只统计班级为 'A' 和 'B' 的学生,并按班级分组。
from itertools import groupby
students = [
{'name': 'Alice', 'class': 'A'},
{'name': 'Bob', 'class': 'B'},
{'name': 'Charlie', 'class': 'A'},
{'name': 'David', 'class': 'B'},
{'name': 'Eve', 'class': 'A'},
{'name': 'Frank', 'class': 'C'} # 我们想排除掉 Frank
]
# 错误的做法:在 key 函数中过滤
# sorted_students = sorted(students, key=lambda x: x['class'] if x['class'] in ('A', 'B') else None)
# 这种排序方式会把 'C' 班的学生排到前面或后面,打乱原有的 'A' 和 'B' 的顺序。
# 正确的做法:
# 1. 先过滤数据
filtered_students = [s for s in students if s['class'] in ('A', 'B')]
# 2. 再排序
sorted_students = sorted(filtered_students, key=lambda x: x['class'])
# 3. 最后分组
for class_name, group_iter in groupby(sorted_students, key=lambda x: x['class']):
print(f"班级: {class_name}")
for student in group_iter:
print(f" - {student['name']}")
groupby vs. pandas.groupby
对于数据分析任务,你可能会遇到 pandas 库的 groupby,它们名字相似,但功能和用法有很大区别。
| 特性 | itertools.groupby |
pandas.groupby |
|---|---|---|
| 来源 | Python 标准库 itertools |
第三方库 pandas |
| 数据类型 | 任何可迭代对象 (列表, 元组等) | pandas.DataFrame 或 pandas.Series |
| 核心功能 | 按连续元素分组,基于迭代 | 按列值分组,基于索引和列,功能极其强大 |
| 排序要求 | 必须预先排序 | 不需要预先排序,它会自动处理 |
| 主要用途 | 简单的、基于内存的、逐个元素的分组和聚合 | 数据分析、统计、数据清洗、复杂的聚合操作 |
| 性能 | 内存效率高,适合流式处理 | 针对大型数据集进行了优化,使用 C 语言后端 |
| 聚合操作 | 手动实现 (如 sum(), len()) |
内置丰富的聚合函数 (.sum(), .mean(), .count(), .agg() 等) |
- 如果你只是处理一个列表或元组,需要对连续的、已排序的元素进行简单分组,用
itertools.groupby。 - 如果你正在处理表格数据(像 Excel),需要进行复杂的分组统计、聚合计算,用
pandas.groupby。
itertools.groupby 是一个非常强大且高效的工具,尤其适用于处理已排序的序列数据,记住它的三大黄金法则:
- 先排序:
groupby只对相邻元素分组。 - 迭代器:
group对象只能遍历一次。 - 按需处理:它是一个惰性求值的迭代器,非常适合处理大数据流。
希望这个详细的解释能帮助你完全理解并掌握 groupby 函数!
