常用标准库:collections

Python 的 collections 模块提供了几个”比内置更高级”的数据结构。它们不是替代品,而是在特定场景下让代码更清晰、更快。前面几篇零散提到过 Counterdefaultdictdequenamedtuple——这一篇把 collections 里六大主角一次讲完。

一、模块概览

用途
Counter 计数专用字典
defaultdict 自动创建缺失 key 的字典
OrderedDict 有序字典(3.7+ 后普通 dict 也保序)
deque 双端队列,两头 O(1) 操作
namedtuple 带字段名的元组
ChainMap 多个字典的链式视图

二、Counter:计数专用字典

前面 字典 Dict 的使用 讲过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from collections import Counter

words = ["a", "b", "a", "c", "b", "a"]
c = Counter(words)
print(c) # Counter({'a': 3, 'b': 2, 'c': 1})

# 直接用字符串(每个字符计数)
Counter("mississippi") # Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})

# 常用方法
c.most_common(2) # [('a', 3), ('b', 2)]
c["z"] # 0,缺失 key 返回 0(不是 KeyError)
c.total() # 6(Python 3.10+)
c.update("aab") # 累加进去
c.subtract("bb") # 减

支持数学运算(元素级):

1
2
3
4
5
6
7
a = Counter(a=3, b=1)
b = Counter(a=1, b=2, c=5)

a + b # Counter({'c': 5, 'a': 4, 'b': 3})
a - b # Counter({'a': 2}) 保留正数
a & b # 交集:最小值
a | b # 并集:最大值

用途

  • 统计词频、字符频、访问量;
  • 找 Top N;
  • 集合的”多重集合”运算。

三、defaultdict:分组神器

自动创建缺失 key,无需先检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from collections import defaultdict

# 分组
groups = defaultdict(list)
for name, dept in employees:
groups[dept].append(name)

# 计数
counter = defaultdict(int)
for word in words:
counter[word] += 1

# 嵌套:dict of dict
matrix = defaultdict(lambda: defaultdict(int))
matrix["a"]["b"] += 1 # 自动初始化

参数是”如何创建缺失值”的可调用对象:

  • int → 缺省 0
  • list → 缺省空 list
  • set → 缺省空 set
  • lambda: ... → 任意逻辑

访问不存在的 key 会自动创建(如果不想触发,用 d.get(k) 而不是 d[k])。

四、OrderedDict:需要顺序还得靠它的时候

Python 3.7+ 普通 dict 已经保持插入顺序,但 OrderedDict 还有一些独有能力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from collections import OrderedDict

od = OrderedDict()
od["a"] = 1
od["b"] = 2
od["c"] = 3

# 移到末尾(最近使用)
od.move_to_end("a") # OrderedDict([('b', 2), ('c', 3), ('a', 1)])
od.move_to_end("b", last=False) # 移到开头

# 弹出末尾/开头
od.popitem(last=True) # 弹末尾
od.popitem(last=False) # 弹开头

# 相等比较严格:顺序也要一样
OrderedDict(a=1, b=2) == OrderedDict(b=2, a=1) # False
dict(a=1, b=2) == dict(b=2, a=1) # True

典型应用:手写 LRU 缓存。

五、deque:双端队列

一个”两端都能 O(1) 增删”的容器。前面 列表 List 详解 提到过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from collections import deque

q = deque()
q.append(1) # 右边加
q.append(2)
q.appendleft(0) # 左边加
print(q) # deque([0, 1, 2])

q.pop() # 右边弹 → 2
q.popleft() # 左边弹 → 0

# 限长队列(超过自动丢弃对端)
tail = deque(maxlen=3)
for i in range(10):
tail.append(i)
print(tail) # deque([7, 8, 9], maxlen=3) 只保留最后 3 个

# 旋转
q = deque([1, 2, 3, 4, 5])
q.rotate(2) # 向右旋转 2 位 → deque([4, 5, 1, 2, 3])
q.rotate(-1) # 向左

用途

  • 队列 / 双端队列;
  • 滑动窗口 / 保留最近 N 个;
  • BFS(广度优先搜索);
  • 循环缓冲。

六、namedtuple:给元组起名字

1
2
3
4
5
6
7
8
9
10
11
12
from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)

p.x, p.y # 3 4
p[0], p[1] # 也能索引
x, y = p # 解包

Point._fields # ('x', 'y')
p._replace(x=10) # 生成新元组(不可变)
p._asdict() # {'x': 3, 'y': 4}

更现代的 typing.NamedTuple 支持类型注解和方法:

1
2
3
4
5
6
7
8
from typing import NamedTuple

class Point(NamedTuple):
x: float
y: float

def distance(self, other):
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5

namedtuple vs dataclass

  • namedtuple:不可变、可解包、可索引、内存小;
  • dataclass:可变(或 frozen 可选)、可加复杂方法、更灵活。

小、只读、频繁创建 → namedtuple;有方法/可变 → dataclass。

七、ChainMap:多个字典的链式视图

想模拟”多层配置”(默认 → 用户 → 环境变量),一层层查找:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from collections import ChainMap

defaults = {"color": "red", "size": "M", "font": "Arial"}
user = {"color": "blue"}
env = {"size": "L"}

config = ChainMap(env, user, defaults) # 从前往后找
print(config["color"]) # 'blue' (在 user 里找到)
print(config["size"]) # 'L' (在 env 里找到)
print(config["font"]) # 'Arial' (fallback 到 defaults)

# 修改总是修改第一个字典
config["color"] = "green"
print(env) # {'size': 'L', 'color': 'green'}

用途:配置层叠、变量作用域模拟。

八、Counter 常见套路

Top N

1
Counter(words).most_common(10)

找出出现 1 次的元素

1
[k for k, v in Counter(items).items() if v == 1]

判断两个序列元素相同(不考虑顺序和重复)

1
Counter(a) == Counter(b)     # anagram 判断

找出多的元素

1
diff = Counter(list1) - Counter(list2)

九、defaultdict 常见套路

反向索引

1
2
3
index = defaultdict(list)
for i, tag in enumerate(tags):
index[tag].append(i)

图的邻接表

1
2
3
4
graph = defaultdict(set)
for u, v in edges:
graph[u].add(v)
graph[v].add(u)

两级分组

1
2
3
groups = defaultdict(lambda: defaultdict(list))
for record in records:
groups[record["region"]][record["type"]].append(record)

十、deque 常见套路

滑动窗口最大值(单调队列)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from collections import deque

def sliding_max(nums, k):
q = deque() # 存索引,保持值降序
result = []
for i, x in enumerate(nums):
while q and q[0] <= i - k:
q.popleft()
while q and nums[q[-1]] < x:
q.pop()
q.append(i)
if i >= k - 1:
result.append(nums[q[0]])
return result


print(sliding_max([1, 3, -1, -3, 5, 3, 6, 7], 3))
# [3, 3, 5, 5, 6, 7]

BFS

1
2
3
4
5
6
7
8
9
10
11
12
from collections import deque

def bfs(graph, start):
visited = {start}
queue = deque([start])
while queue:
node = queue.popleft()
yield node
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)

十一、UserDict / UserList / UserString

想自定义 dict/list/str 但不想踩”直接继承内置类型”的坑:

1
2
3
4
5
6
7
8
9
10
from collections import UserDict

class LoggedDict(UserDict):
def __setitem__(self, key, value):
print(f"设置 {key}={value}")
super().__setitem__(key, value)


d = LoggedDict()
d["a"] = 1 # 设置 a=1

直接 class LoggedDict(dict) 也能用,但内置方法可能不走你的 __setitem__——比如 dict.update 就跳过。UserDict 是”纯 Python 实现”,所有操作都过你重写的方法。

十二、性能对比

  • defaultdict vs dict.setdefault:性能相近,defaultdict 稍快,代码更漂亮;
  • Counter vs 手写循环 + dict:Counter 稍慢一点,但简洁得多;
  • deque vs list:两端操作 deque 快 100 倍;随机索引 list 快;
  • namedtuple vs dataclass:namedtuple 更快、更省内存;dataclass 更灵活。

十三、一个完整例子:URL 访问统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# stats.py
"""日志分析工具,展示 Counter / defaultdict / deque / namedtuple 的组合。"""

from collections import Counter, defaultdict, deque, namedtuple
from datetime import datetime


LogEntry = namedtuple("LogEntry", ["ts", "ip", "path", "status", "elapsed_ms"])


def parse_log(line: str) -> LogEntry | None:
"""
简化格式:
2025-09-27T10:00:00 1.2.3.4 GET /index 200 15
"""
parts = line.split()
if len(parts) != 6:
return None
return LogEntry(
ts=datetime.fromisoformat(parts[0]),
ip=parts[1],
path=parts[3],
status=int(parts[4]),
elapsed_ms=int(parts[5]),
)


def analyze(lines):
ip_counter = Counter()
path_by_status = defaultdict(Counter)
recent = deque(maxlen=100)
slow = deque(maxlen=5) # 最慢的 5 条

for line in lines:
entry = parse_log(line)
if entry is None:
continue

ip_counter[entry.ip] += 1
path_by_status[entry.status][entry.path] += 1
recent.append(entry)

# 保留最慢
slow.append(entry)
slow = deque(sorted(slow, key=lambda e: e.elapsed_ms, reverse=True)[:5], maxlen=5)

return {
"top_ips": ip_counter.most_common(10),
"errors_by_path": dict(path_by_status[500].most_common(5)),
"recent": list(recent)[-5:],
"slowest": list(slow),
}


if __name__ == "__main__":
sample = [
"2025-09-27T10:00:00 1.2.3.4 GET /a 200 12",
"2025-09-27T10:00:01 1.2.3.4 GET /b 200 20",
"2025-09-27T10:00:02 5.6.7.8 GET /a 500 150",
"2025-09-27T10:00:03 5.6.7.8 GET /c 200 500",
]
print(analyze(sample))

十四、常见陷阱

  1. defaultdict 触发副作用d[k] 会创建 key,即使你只想”查”。用 d.get(k) 只查。
  2. Counter 减法保留正数:如果想保留所有(含负),用 c.subtract(other)
  3. deque(maxlen=N):加入超出的元素时对端会被丢弃。别忘了。
  4. namedtuple 不可变:改字段用 ._replace() 返回新对象。
  5. OrderedDict 相等比较:顺序不同就 !=。
  6. ChainMap 修改:永远动第一个 dict。想改深层要指定:chain.maps[i][k] = ...

十五、小结与延伸阅读

  • Counter:计数、Top N、多重集合运算;
  • defaultdict:分组、计数、嵌套结构;
  • OrderedDict:需要 move_to_end / popitem(last) 时用;
  • deque:两端 O(1)、限长、滑动窗口、BFS;
  • namedtuple:轻量记录,比字典更结构化,比 class 更省;
  • ChainMap:多层配置查找;
  • UserDict/UserList/UserString:更安全的继承。

延伸阅读:

下一篇 常用标准库:itertools 与 functools 我们讲两个函数式工具库。