前几天帮新人看代码,他写了整整 80 行来统计一个列表里每个元素出现的次数。我指着屏幕跟他说,这活儿一行就能干完。
他不信。
我把他的代码全删了,写了一行:
from collections import Counter
counts = Counter(words)
他看着屏幕沉默了大概五秒钟,然后问了一句:这个 Counter 是什么神仙东西?
说实话,我特别理解他的感受。因为三年前的我也一样——天天写 dict、list、set,觉得 Python 的数据结构就这些了,完全不知道 collections 模块下面藏着一堆好用的家伙。
今天就聊聊这些被我忽略了三年的「隐藏装备」。
Counter:统计从来不需要循环
Counter 是我日常用得最多的一个。它的本质是一个字典的子类,专门用来计数。
比如你要统计一段文本里每个词出现的频率:
from collections import Counter
text = "苹果 香蕉 苹果 橘子 香蕉 苹果 葡萄 橘子 苹果"
words = text.split()
counts = Counter(words)
print(counts)
# Counter({'苹果': 4, '香蕉': 2, '橘子': 2, '葡萄': 1})
# 最常见的两个
print(counts.most_common(2))
# [('苹果', 4), ('香蕉', 2)]
输出结果:
Counter({'苹果': 4, '香蕉': 2, '橘子': 2, '葡萄': 1})
[('苹果', 4), ('香蕉', 2)]
但这只是最基础的用法。真正让我觉得「绝了」的是它的运算能力。
比如你有两份日志,想知道两天的差异:
day1 = Counter(['error', 'warning', 'error', 'info', 'warning'])
day2 = Counter(['error', 'error', 'info', 'info', 'debug'])
# 两天新增了什么
print(day2 - day1)
# Counter({'info': 1, 'debug': 1})
# 两天共同出现的
print(day1 & day2)
# Counter({'error': 2, 'info': 1})
# 两天的汇总
print(day1 | day2)
# Counter({'error': 3, 'info': 2, 'warning': 2, 'debug': 1})
输出:
Counter({'info': 1, 'debug': 1})
Counter({'error': 2, 'info': 1})
Counter({'error': 3, 'info': 2, 'warning': 2, 'debug': 1})
以前做这种对比,我要写两层循环,搞不好还要处理 key 不存在的情况。现在?一行减法搞定。
还有一个实用场景——找列表中的重复元素:
data = ['a', 'b', 'c', 'a', 'd', 'b', 'a', 'e']
# 出现超过1次的元素
duplicates = [item for item, count in Counter(data).items() if count > 1]
print(duplicates)
# ['a', 'b']
defaultdict:告别 KeyError 的噩梦
你有没有被 KeyError 折磨过?
# 传统写法,每次都要判断
groups = {}
for name, score in scores:
if name not in groups:
groups[name] = []
groups[name].append(score)
这种 if key not in dict 的写法,我以前写了不下一百遍。直到我遇到了 defaultdict:
from collections import defaultdict
groups = defaultdict(list)
for name, score in scores:
groups[name].append(score)
就这么简单。defaultdict(list) 的意思是:当你访问一个不存在的 key 时,自动创建一个空列表。不用判断,不用初始化,直接往里塞就行。
除了 list,还可以用 int、set、str:
# 自动计数
counter = defaultdict(int)
for item in items:
counter[item] += 1
# 自动去重分组
unique_groups = defaultdict(set)
for name, tag in data:
unique_groups[name].add(tag)
我之前做过一个需求:把一批用户按「注册月份」分组。用 defaultdict 三行搞定:
from collections import defaultdict
users = [
{'name': '张三', 'date': '2026-01'},
{'name': '李四', 'date': '2026-01'},
{'name': '王五', 'date': '2026-02'},
{'name': '赵六', 'date': '2026-03'},
{'name': '钱七', 'date': '2026-01'},
]
by_month = defaultdict(list)
for u in users:
by_month[u['date']].append(u['name'])
for month, names in sorted(by_month.items()):
print(f"{month}: {names}")
输出:
2026-01: ['张三', '李四', '钱七']
2026-02: ['王五']
2026-03: ['赵六']
deque:队列场景的终极答案
deque(双端队列)是另一个被我严重低估的结构。
有个经典场景:你要维护一个「最近 N 条记录」的滑动窗口。用 list 的话,每次 pop(0) 都是 O(n) 的时间复杂度,数据量大的时候慢得让你想砸键盘。
但 deque 的 popleft() 是 O(1):
from collections import deque
# 最近5条操作日志
recent_logs = deque(maxlen=5)
operations = ['登录', '查询', '修改', '删除', '导出', '登录', '查询']
for op in operations:
recent_logs.append(op)
print(f"当前: {list(recent_logs)}")
输出:
当前: ['登录']
当前: ['登录', '查询']
当前: ['登录', '查询', '修改']
当前: ['登录', '查询', '修改', '删除']
当前: ['登录', '查询', '修改', '删除', '导出']
当前: ['查询', '修改', '删除', '导出', '登录']
当前: ['修改', '删除', '导出', '登录', '查询']
看到没?当第六个元素进来的时候,第一个自动被踢出去了。不用手动判断长度,不用 pop(0),deque 自己就管好了。
deque 还支持从左边操作:
d = deque([2, 3, 4])
d.appendleft(1) # 左边加
d.append(5) # 右边加
d.popleft() # 左边弹
d.pop() # 右边弹
print(d)
# deque([2, 3, 4])
实际场景中,我用 deque 最多的是做「最近 N 次请求的限流」——记录每个用户最近 10 次请求时间,超过阈值就拒绝。代码简洁得令人发指。
namedtuple:不想写 class 时的优雅选择
有时候你需要一个简单的数据容器,只有几个字段,不想定义一个完整的类。以前我会用字典或者元组,但字典没有字段提示,元组只有索引没有名字。
namedtuple 完美解决了这个问题:
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p1 = Point(3, 4)
print(p1.x, p1.y) # 3 4
print(p1[0], p1[1]) # 3 4 索引也可以用
print(p1) # Point(x=3, y=4)
我经常用它来处理从数据库或 API 返回的扁平数据:
User = namedtuple('User', ['id', 'name', 'email', 'role'])
raw_data = [
(1, '张三', 'zhangsan@mail.com', 'admin'),
(2, '李四', 'lisi@mail.com', 'user'),
(3, '王五', 'wangwu@mail.com', 'user'),
]
users = [User(*row) for row in raw_data]
# 找出所有管理员
admins = [u for u in users if u.role == 'admin']
print(admins)
# [User(id=1, name='张三', email='zhangsan@mail.com', role='admin')]
比裸元组可读性强太多了,而且它支持 ._asdict() 转字典,._make() 从列表创建,非常灵活。
写在最后
说实话,collections 模块里的这些东西,每一个单独拿出来都不复杂。但真正让我觉得「相见恨晚」的不是它们本身,而是它们代表的思维方式——
不要重复造轮子。
很多我们觉得「只能手写循环」的场景,标准库里早就有了更优的解法。问题不在于这些工具难学,而在于你根本不知道它们存在。
所以我的建议是:每隔一段时间,花半小时翻一遍你常用模块的文档。你不需要记住所有 API,你只需要知道「哦,原来还有这个东西」——下次遇到合适的场景,你自然会想到去用它。
最后,别忘了关注「有为大青年」,我们下期见~