1. 引言:为什么需要 collections
Python 内置的 list、dict、set、tuple 已经能覆盖大部分日常需求,但在实际工程中,我们经常遇到一些"高频且模式固定"的场景:
- 统计词频、计数、投票排名;
- 按 key 分组聚合,避免反复写
if key not in d: d[key] = []; - 需要一个"记住插入顺序"的字典;
- 实现队列、双端队列、滑动窗口;
- 用更可读的方式表示一条记录,而不是
record[0]、record[1]。
collections 模块正是为这些场景准备的标准库工具箱。它不引入第三方依赖,性能经过 CPython 底层优化,是"写得更少、跑得更快、读得更懂"的典型代表。
本文会依次介绍 Counter、defaultdict、OrderedDict、deque、namedtuple 五个核心容器,说明各自的适用场景,最后用"词频统计"和"队列任务"两个完整案例把它们串起来。
2. collections 模块全景
collections 中常用的容器可以按用途粗略分类:
| 容器 | 本质 | 典型用途 |
|---|---|---|
Counter | dict 子类 | 计数、词频、Top-N 排名 |
defaultdict | dict 子类 | 分组聚合、图结构、嵌套字典 |
OrderedDict | dict 子类 | 需要显式顺序语义、LRU 缓存 |
deque | 双端队列 | 队列、栈、滑动窗口、BFS |
namedtuple | tuple 子类 | 轻量不可变记录、替代小类 |
它们都来自标准库,导入方式统一:
from collections import Counter, defaultdict, OrderedDict, deque, namedtuple
下面逐个展开。
下面这张图展示了从需求场景到具体容器的选型路径:
flowchart TD
A[需求场景] --> B{需要计数或排名}
B -->|是| C[Counter]
B -->|否| D{需要分组或嵌套字典}
D -->|是| E[defaultdict]
D -->|否| F{需要队列或滑动窗口}
F -->|是| G[deque]
F -->|否| H{需要轻量不可变记录}
H -->|是| I[namedtuple]
H -->|否| J{需要顺序敏感或缓存}
J -->|是| K[OrderedDict]
J -->|否| L[内置字典或列表即可]
3. Counter:计数器
3.1 基本用法
Counter 是一个专门用于计数的字典,key 是元素,value 是出现次数。
from collections import Counter
words = ["apple", "banana", "apple", "orange", "banana", "apple"]
c = Counter(words)
print(c) # Counter({'apple': 3, 'banana': 2, 'orange': 1})
print(c["apple"]) # 3
print(c["grape"]) # 0,不存在的 key 返回 0 而不是 KeyError
注意最后一行:访问不存在的元素时,Counter 返回 0 而不是抛异常,这让计数逻辑非常省心。
3.2 常用方法
from collections import Counter
c = Counter("abracadabra")
# 出现次数最多的 2 个
print(c.most_common(2)) # [('a', 5), ('b', 2)]
# 元素总数(含重复)
print(c.total()) # 11(Python 3.10+)
# 更新计数
c.update(["a", "a", "z"])
print(c["a"], c["z"]) # 7 1
# 减去计数
c.subtract(["a"])
print(c["a"]) # 6
# 转为普通字典
print(dict(c))
most_common(n) 是 Counter 最常用的方法之一,内部使用 heapq.nlargest 实现,取 Top-N 时效率很高;不传参数时返回全部元素按计数降序排列的列表。
3.3 计数运算
Counter 支持集合式的加减交并运算,适合做多集合的计数合并:
from collections import Counter
c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2)
print(c1 + c2) # Counter({'a': 4, 'b': 3})
print(c1 - c2) # Counter({'a': 2})
print(c1 & c2) # Counter({'a': 1, 'b': 1}),取较小值
print(c1 | c2) # Counter({'a': 3, 'b': 2}),取较大值
注意 - 运算会丢弃结果小于等于 0 的项,这符合"计数不能为负"的直觉;而 & 取两者较小值、| 取两者较大值,同样会丢弃计数为 0 的项。
3.4 适用场景
- 词频统计、日志级别统计、投票计数;
- 找出出现频率最高的 N 个元素;
- 多批次数据的计数累加;
- 判断两个集合的元素重合程度。
4. defaultdict:带默认值的字典
4.1 解决什么问题
普通字典在聚合时总要判断 key 是否存在:
groups = {}
for name, dept in [("Tom", "RD"), ("Jerry", "RD"), ("Lucy", "HR")]:
if dept not in groups:
groups[dept] = []
groups[dept].append(name)
defaultdict 把这段样板代码消掉:
from collections import defaultdict
groups = defaultdict(list)
for name, dept in [("Tom", "RD"), ("Jerry", "RD"), ("Lucy", "HR")]:
groups[dept].append(name)
print(dict(groups)) # {'RD': ['Tom', 'Jerry'], 'HR': ['Lucy']}
4.2 default_factory 的几种常见写法
from collections import defaultdict
# 值为列表:分组
d1 = defaultdict(list)
d1["a"].append(1)
# 值为整数:计数
d2 = defaultdict(int)
for ch in "hello":
d2[ch] += 1
print(dict(d2)) # {'h': 1, 'e': 1, 'l': 2, 'o': 1}
# 值为集合:去重分组
d3 = defaultdict(set)
d3["x"].add(1)
d3["x"].add(1)
print(dict(d3)) # {'x': {1}}
# 值为字典:嵌套结构
d4 = defaultdict(dict)
d4["user"]["name"] = "Tom"
print(dict(d4)) # {'user': {'name': 'Tom'}}
4.3 注意事项
defaultdict 在访问不存在的 key 时会自动创建该 key,这既是优点也是坑:
from collections import defaultdict
d = defaultdict(int)
_ = d["not_exist"] # 访问即创建
print(d) # defaultdict(<class 'int'>, {'not_exist': 0})
如果只是想"查询而不想写入",应使用 d.get(key) 或 key in d 判断,避免无意中污染字典。
4.4 适用场景
- 按字段分组聚合(日志按级别、订单按用户);
- 构建邻接表表示图;
- 多层嵌套字典(如
d[year][month][day]); - 替代
dict.setdefault的繁琐写法。
5. OrderedDict:有序字典
5.1 与普通 dict 的区别
从 Python 3.7 起,内置 dict 已经保证插入顺序,因此 OrderedDict 的"保序"能力不再是独有优势。但它仍有两个差异点:
- 顺序敏感的相等比较:两个
OrderedDict只有在顺序也相同时才相等,而普通dict只比较键值。 move_to_end方法:可以把某个 key 移到末尾或开头,这是实现 LRU 缓存的关键。
from collections import OrderedDict
od = OrderedDict()
od["a"] = 1
od["b"] = 2
od["c"] = 3
od.move_to_end("a") # 移到末尾
print(list(od.keys())) # ['b', 'c', 'a']
od.move_to_end("a", last=False) # 移到开头
print(list(od.keys())) # ['a', 'b', 'c']
5.2 用 OrderedDict 实现 LRU 缓存
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.data = OrderedDict()
def get(self, key):
if key not in self.data:
return -1
self.data.move_to_end(key) # 访问后移到末尾,表示最近使用
return self.data[key]
def put(self, key, value):
if key in self.data:
self.data.move_to_end(key)
self.data[key] = value
if len(self.data) > self.capacity:
self.data.popitem(last=False) # 淘汰最久未使用的
cache = LRUCache(2)
cache.put("a", 1)
cache.put("b", 2)
cache.get("a") # 返回 1,a 变为最近使用
cache.put("c", 3) # 淘汰 b
print(list(cache.data.keys())) # ['a', 'c']
LRU 缓存的核心逻辑可以用下面的流程图表示:
flowchart TD
A[读取键] --> B{键是否存在}
B -->|否| C[返回负一]
B -->|是| D[移到末尾标记为最近使用]
D --> E[返回对应值]
F[写入键值] --> G{键已存在}
G -->|是| H[移到末尾]
G -->|否| I[直接写入]
H --> J[写入新值]
I --> J
J --> K{长度超过容量}
K -->|是| L[淘汰最久未使用项]
K -->|否| M[结束]
L --> M
5.3 适用场景
- 需要显式表达"顺序有意义"的语义;
- LRU / FIFO 缓存淘汰;
- 需要顺序敏感比较的配置或序列化场景。
如果只是"想要一个保序字典",直接用内置 dict 即可,不必引入 OrderedDict。
6. deque:双端队列
6.1 为什么不用 list 做队列
list 的 pop(0) 和 insert(0, x) 都是 O(n) 操作,因为要移动后续所有元素。deque 底层是由固定长度块组成的双向链表结构,两端插入删除都是 O(1),且 append/pop 在 CPython 中由 C 实现,常数因子很小。
from collections import deque
dq = deque([1, 2, 3])
dq.append(4) # 右端入队
dq.appendleft(0) # 左端入队
print(dq) # deque([0, 1, 2, 3, 4])
print(dq.pop()) # 4,右端出队
print(dq.popleft()) # 0,左端出队
print(dq) # deque([1, 2, 3])
6.2 常用操作
from collections import deque
dq = deque([1, 2, 3, 4, 5], maxlen=5)
dq.rotate(1) # 整体右移一位
print(dq) # deque([5, 1, 2, 3, 4])
dq.rotate(-1) # 整体左移一位
print(dq) # deque([1, 2, 3, 4, 5])
dq.extend([6, 7]) # 右端批量追加
print(dq) # deque([3, 4, 5, 6, 7]),maxlen 自动丢弃左侧
maxlen 参数非常实用:当队列满时,新元素入队会自动从另一端丢弃旧元素,天然适合"最近 N 条记录"的场景。需要注意 maxlen 一旦创建就不可修改,且 insert 在达到上限时会直接抛出 IndexError。
6.3 适用场景
- 队列 / 栈(
append+pop即栈,append+popleft即队列); - BFS 广度优先搜索的待访问节点;
- 滑动窗口最大值、最近 N 条日志;
- 生产者-消费者模型中的缓冲区。
7. namedtuple:具名元组
7.1 基本用法
namedtuple 生成一个带字段名的元组子类,既有元组的轻量与不可变,又能用属性访问:
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(1, 2)
print(p.x, p.y) # 1 2
print(p[0], p[1]) # 1 2,仍支持下标
print(p) # Point(x=1, y=2)
7.2 常用特性
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(1, 2)
# 解包
x, y = p
print(x, y) # 1 2
# 转字典
print(p._asdict()) # {'x': 1, 'y': 2}
# 替换字段,返回新对象
p2 = p._replace(x=10)
print(p2) # Point(x=10, y=2)
# 带默认值
Point3 = namedtuple("Point3", ["x", "y", "z"], defaults=[0])
print(Point3(1, 2)) # Point3(x=1, y=2, z=0)
7.3 适用场景
- 表示坐标、数据库行、CSV 记录等轻量数据结构;
- 函数返回多个值时,用
namedtuple比裸元组更可读; - 替代只有字段没有方法的"数据类"(若需要可变或更多功能,可考虑
dataclasses.dataclass)。
8. 五个容器对比与选型
| 容器 | 是否可变 | 是否有序 | 访问复杂度 | 一句话选型 |
|---|---|---|---|---|
Counter | 可变 | 是 | O(1) | 要计数就用它 |
defaultdict | 可变 | 是 | O(1) | 要分组/嵌套就用它 |
OrderedDict | 可变 | 是(顺序敏感) | O(1) | 要 LRU 或顺序语义就用它 |
deque | 可变 | 是 | 两端 O(1) | 要队列/滑动窗口就用它 |
namedtuple | 不可变 | 是 | O(1) | 要轻量记录就用它 |
选型口诀:计数找 Counter,分组找 defaultdict,队列找 deque,记录找 namedtuple,顺序敏感找 OrderedDict。
9. 实战案例一:词频统计
需求:统计一段文本中单词出现频率,输出 Top-10,并统计每个首字母下的单词分布。
import re
from collections import Counter, defaultdict
text = """
Python is simple. Python is powerful.
Simple is better than complex. Complex is better than complicated.
Readability counts. Python counts readability.
"""
# 1. 分词并统一小写
words = re.findall(r"[a-zA-Z]+", text.lower())
# 2. 词频统计
counter = Counter(words)
print("Top-10 高频词:")
for word, cnt in counter.most_common(10):
print(f" {word:<12} {cnt}")
# 3. 按首字母分组
by_letter = defaultdict(list)
for word in counter:
by_letter[word[0]].append(word)
print("\n按首字母分组:")
for letter in sorted(by_letter):
print(f" {letter}: {sorted(by_letter[letter])}")
# 4. 统计总词数
print("\n总词数:", sum(counter.values()))
这个案例同时用到了 Counter(计数与 Top-N)和 defaultdict(分组),是两者最经典的组合。
词频统计的处理链路可以概括为:
flowchart LR
A[原始文本] --> B[正则分词并小写化]
B --> C[Counter 计数]
C --> D[取前十高频词]
C --> E[遍历键按首字母分组]
E --> F[defaultdict 聚合]
D --> G[输出高频词]
F --> H[输出字母分布]
10. 实战案例二:队列任务处理
需求:模拟一个任务队列,支持优先级、失败重试、以及记录最近 N 条处理日志。
from collections import deque, namedtuple, Counter
# 用 namedtuple 定义任务结构
Task = namedtuple("Task", ["task_id", "name", "priority", "retries"])
# 待处理队列与最近日志
pending = deque()
recent_logs = deque(maxlen=5)
status_counter = Counter()
def submit(task: Task):
# 高优先级插到队首,普通任务追加到队尾
if task.priority >= 8:
pending.appendleft(task)
else:
pending.append(task)
def process():
while pending:
task = pending.popleft()
try:
# 模拟处理:优先级低于 5 的任务视为失败
if task.priority < 5:
raise RuntimeError("low priority task failed")
status_counter["success"] += 1
recent_logs.append(f"OK {task.name}")
except RuntimeError as e:
if task.retries > 0:
# 重试:重试次数减一,重新入队
pending.append(task._replace(retries=task.retries - 1))
status_counter["retry"] += 1
recent_logs.append(f"RETRY {task.name}")
else:
status_counter["failed"] += 1
recent_logs.append(f"FAIL {task.name}: {e}")
# 提交任务
submit(Task(1, "send_email", 9, 1))
submit(Task(2, "gen_report", 3, 2))
submit(Task(3, "sync_data", 6, 0))
submit(Task(4, "clean_cache", 2, 0))
process()
print("处理结果统计:", dict(status_counter))
print("最近日志:")
for log in recent_logs:
print(" ", log)
这个案例把三个容器串了起来:
namedtuple定义不可变的任务记录,_replace生成重试任务;deque作为双端队列,高优先级任务从左侧插入,maxlen自动保留最近日志;Counter统计成功、重试、失败次数。
flowchart TD
A[提交任务] --> B{优先级大于等于八}
B -->|是| C[插入队首]
B -->|否| D[追加队尾]
C --> E[循环处理]
D --> E
E --> F{队列非空}
F -->|否| Z[结束]
F -->|是| G[取出任务]
G --> H{优先级小于五}
H -->|否| I[成功计数并记录日志]
H -->|是| J{剩余重试次数大于零}
J -->|是| K[重试次数减一后重新入队]
J -->|否| L[失败计数并记录日志]
I --> F
K --> F
L --> F
11. 性能与最佳实践
- 优先用标准库:
Counter、defaultdict、deque的底层实现经过优化,通常比手写循环更快、更省内存。 - 避免误用
defaultdict的自动创建:只查询时用get或in,防止字典被意外填充。 - 队列不要用 list:
list.pop(0)是 O(n),数据量大时性能差距明显。 OrderedDict不是保序首选:Python 3.7+ 直接用dict即可,只有需要move_to_end或顺序敏感比较时才用它。namedtuple适合不可变数据:需要可变字段或方法时,考虑dataclasses.dataclass。Counter.most_common(n)优于sorted:取 Top-N 时前者使用堆,复杂度更优。
12. 总结
collections 模块用五个小而精的容器,覆盖了 Python 日常开发中最常见的几类数据结构需求:
Counter让计数从"写循环"变成"一行调用";defaultdict消除了分组聚合时的样板判断;OrderedDict在需要顺序语义或 LRU 时仍然不可替代;deque是队列、栈、滑动窗口的正确选择;namedtuple用极低成本换来可读性。
掌握它们的关键不是记住 API,而是建立"场景 → 容器"的条件反射。下次写代码时,如果发现自己又在写 if key not in d、list.pop(0) 或 record[0],不妨停下来想一想:这里是不是该用 collections?