Python collections:高效容器与实战案例

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 子类 轻量不可变记录、替代小类

它们都来自标准库,导入方式统一:

python 复制代码
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 是出现次数。

python 复制代码
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 常用方法

python 复制代码
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 支持集合式的加减交并运算,适合做多集合的计数合并:

python 复制代码
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 是否存在:

python 复制代码
groups = {}
for name, dept in [("Tom", "RD"), ("Jerry", "RD"), ("Lucy", "HR")]:
    if dept not in groups:
        groups[dept] = []
    groups[dept].append(name)

defaultdict 把这段样板代码消掉:

python 复制代码
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 的几种常见写法

python 复制代码
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,这既是优点也是坑:

python 复制代码
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 的"保序"能力不再是独有优势。但它仍有两个差异点:

  1. 顺序敏感的相等比较 :两个 OrderedDict 只有在顺序也相同时才相等,而普通 dict 只比较键值。
  2. move_to_end 方法:可以把某个 key 移到末尾或开头,这是实现 LRU 缓存的关键。
python 复制代码
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 缓存

python 复制代码
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 实现,常数因子很小。

python 复制代码
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 常用操作

python 复制代码
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 生成一个带字段名的元组子类,既有元组的轻量与不可变,又能用属性访问:

python 复制代码
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 常用特性

python 复制代码
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,并统计每个首字母下的单词分布。

python 复制代码
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 条处理日志。

python 复制代码
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. 性能与最佳实践

  1. 优先用标准库 :Counter、defaultdict、deque 的底层实现经过优化,通常比手写循环更快、更省内存。
  2. 避免误用 defaultdict 的自动创建 :只查询时用 get 或 in,防止字典被意外填充。
  3. 队列不要用 list :list.pop(0) 是 O(n),数据量大时性能差距明显。
  4. OrderedDict 不是保序首选 :Python 3.7+ 直接用 dict 即可,只有需要 move_to_end 或顺序敏感比较时才用它。
  5. namedtuple 适合不可变数据 :需要可变字段或方法时,考虑 dataclasses.dataclass。
  6. 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?

相关推荐
驭渊的小故事27 分钟前
Spring AOP 核心概念与通知类型详解
java·后端·spring
vipxieliang28 分钟前
ValidX vs 手写验证代码:开发效率对比实测
java·spring boot·后端
青少儿编程课堂28 分钟前
蓝桥杯青少组图形化 STEMA 省考(报名倒计时·11/22开考)+ 全国青少年信息素养大赛 2026—2027赛季 + 电子学会图形化考级:校园华容道大闯关
c++·python·算法·bfs·信息学竞赛
特创数字科技37 分钟前
写了个 Windows 下载器:一键拉 GitHub / Gitee 源码,带 SHA256 校验
python·github
vipxieliang39 分钟前
Go GC 性能优化实践:从 GOGC、GOMEMLIMIT 到分配优化
后端·go
海豚调度1 小时前
Apache DolphinScheduler 2026 年 10 月社区贡献总结报告
后端
vipxieliang1 小时前
ThinkORM 3.0 入门:强类型模型定义与查询构造器
后端·php·thinkphp
vipxieliang1 小时前
虚拟线程与 GraalVM:Spring Boot 3.x 云原生性能跃迁
java·spring boot·后端
Rain的Java大神之路1 小时前
面试官最烦的答案:“我用 Redis + Token”!接口幂等 8 种方案 + 四层防御,把重复扣款工单干到 0
java·后端·面试