文章目录
-
-
- [第一阶段:为什么需要 collections?(背景与痛点)](#第一阶段:为什么需要 collections?(背景与痛点))
-
- [1. 场景引入:内置类型的"笨重"操作](#1. 场景引入:内置类型的“笨重”操作)
- [2. collections 的解法:更聪明的容器](#2. collections 的解法:更聪明的容器)
- 第二阶段:核心功能全景图
- 第三阶段:逐功能实战学习
-
- [🔹 1. `Counter`:计数器的终极形态](#🔹 1.
Counter:计数器的终极形态) - [🔹 2. `defaultdict`:告别 `KeyError` 的烦恼](#🔹 2.
defaultdict:告别KeyError的烦恼) - [🔹 3. `deque`:双端队列,性能怪兽](#🔹 3.
deque:双端队列,性能怪兽) - [🔹 4. `namedtuple`:带字段名的元组](#🔹 4.
namedtuple:带字段名的元组) - [🔹 5. `ChainMap`:多字典合并视图](#🔹 5.
ChainMap:多字典合并视图) - [🔹 6. `OrderedDict`:有序字典的额外能力](#🔹 6.
OrderedDict:有序字典的额外能力)
- [🔹 1. `Counter`:计数器的终极形态](#🔹 1.
-
第一阶段:为什么需要 collections?(背景与痛点)
1. 场景引入:内置类型的"笨重"操作
假设你要统计一篇文章中每个单词出现的次数。用最基础的内置 dict,你需要这样写:
python
# 😩 传统做法:手动处理"键不存在"的异常
text = "apple banana apple orange banana apple"
word_count = {}
for word in text.split():
if word not in word_count:
word_count[word] = 0 # 每次都要判断键是否存在
word_count[word] += 1
print(word_count)
# {'apple': 3, 'banana': 2, 'orange': 1}
或者,你可能想用 list 来记录用户的操作历史,但当你需要频繁在头部插入数据时:
python
# 😩 传统做法:列表头部插入性能极差
history = []
history.insert(0, "login") # O(n) 复杂度,每次都要把后面的元素往后挪
history.insert(0, "click")
history.insert(0, "logout")
🧪 动手验证 :想直观体验
listvsdeque的速度差异,见后文deque小节的计时代码。
痛点总结:
- 字典初始化繁琐 :统计计数、分组数据时,总要写
if key not in dict或dict.get(key, [])。 - 列表性能瓶颈 :
list在尾部追加很快,但在头部插入/删除极慢(因为底层是连续内存数组)。 - 缺乏语义化容器:内置类型太通用,无法表达"这是一个固定大小的滑动窗口"或"这是一个带默认值的字典"。
2. collections 的解法:更聪明的容器
collections 模块提供了一系列针对特定场景优化的容器类,它们继承自内置类型,但增加了强大的功能:
python
from collections import Counter
# ✅ 优雅做法:一行搞定计数
text = "apple banana apple orange banana apple"
word_count = Counter(text.split())
print(word_count)
# Counter({'apple': 3, 'banana': 2, 'orange': 1})
print(word_count.most_common(2)) # [('apple', 3), ('banana', 2)]
🧪 动手验证 1 :分别运行传统
dict和Counter版本。对比代码行数,然后尝试用Counter的most_common()方法获取 Top 3 的单词。
第二阶段:核心功能全景图
| 容器类型 | 解决的问题 | 关键词 |
|---|---|---|
Counter |
高效计数、统计频率 | Counter, most_common() |
defaultdict |
自动初始化缺失键、消除 KeyError |
default_factory |
deque |
双端高效插入/删除、滑动窗口 | appendleft(), popleft() |
OrderedDict |
记住键的插入顺序(Python 3.7+ 普通 dict 也有序,但 OrderedDict 有额外方法) | move_to_end(), popitem() |
namedtuple |
轻量级不可变对象、带字段名的元组 | ._fields, ._asdict() |
ChainMap |
合并多个字典为一个视图、配置优先级 | maps, 作用域链 |
第三阶段:逐功能实战学习
🔹 1. Counter:计数器的终极形态
Counter 是 dict 的子类,专门用于哈希对象计数。它不仅能统计,还提供了丰富的数学运算。
python
from collections import Counter
# ✅ 基础计数
c = Counter("abracadabra")
print(c) # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
# ✅ 获取 Top N
print(c.most_common(3)) # [('a', 5), ('b', 2), ('r', 2)]
# ✅ 计数器算术运算(极其强大)
c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2, c=3)
print(c1 + c2) # Counter({'a': 4, 'b': 3, 'c': 3}) ← 合并计数
print(c1 - c2) # Counter({'a': 2}) ← 差集(只保留正数)
print(c1 & c2) # Counter({'a': 1, 'b': 1}) ← 交集(取最小值)
print(c1 | c2) # Counter({'a': 3, 'b': 2, 'c': 3}) ← 并集(取最大值)
⚠️ 关键规则 :
Counter的算术运算会自动丢弃零值和负值结果。
💡 除了most_common(),Counter 还有几个非常常用的能力:
- 缺失键默认返回 0 :
c["missing"]不会 KeyError,而是0- 增量统计
update():适合分批/流式累加- 扣减
subtract():从总计中减去一批数据(可能出现负数计数)- 展开
elements():把计数"还原"为元素序列(只展开计数 > 0 的项)
pythonfrom collections import Counter c = Counter("abca") print(c["x"]) # 0(缺失键默认 0) c.update("bbb") # 增量统计 print(c) # Counter({'b': 4, 'a': 2, 'c': 1}) c.subtract("abbbbb") # 扣减(可能产生负数) print(c["b"]) # -1(注意:subtract 不会自动丢弃负数) print(list(c.elements())) # 只会展开计数 > 0 的元素
🧪 动手验证 2 :用两个 Counter 分别统计两段文本的词频,然后用 - 运算找出第一段文本独有的词。
🔹 2. defaultdict:告别 KeyError 的烦恼
当你需要一个字典,且访问不存在的键时希望自动创建默认值,defaultdict 是你的救星。
python
from collections import defaultdict
# ✅ 分组操作(最经典场景)
students = [
("Math", "Alice"), ("Math", "Bob"),
("English", "Charlie"), ("Math", "David"),
("English", "Eve")
]
# 😩 传统做法
groups = {}
for subject, name in students:
if subject not in groups:
groups[subject] = []
groups[subject].append(name)
# ✅ defaultdict 做法
groups = defaultdict(list) # 传入 list 作为默认工厂
for subject, name in students:
groups[subject].append(name) # 无需判断,键不存在时自动调用 list() 创建空列表
print(dict(groups))
# {'Math': ['Alice', 'Bob', 'David'], 'English': ['Charlie', 'Eve']}
⚠️ 访问副作用(非常重要) :
defaultdict访问一个不存在的键会"顺便创建该键"。
pythonfrom collections import defaultdict d = defaultdict(list) print("Sci" in d) # False _ = d["Sci"] # 读一次不存在键 → 自动创建 print("Sci" in d) # True如果你只是想"安全读取但不希望创建新键",用
d.get("Sci")更合适。
💡default_factory的灵活性:
defaultdict(list)→ 缺失键自动创建[]defaultdict(int)→ 缺失键自动创建0(等价于 Counter 的简化版)defaultdict(set)→ 缺失键自动创建set()defaultdict(lambda: "N/A")→ 缺失键自动返回自定义默认值
🧪 动手验证 3 :创建一个 defaultdict(lambda: {"count": 0, "names": []}),用它同时记录每个类别的数量和成员名。
🔹 3. deque:双端队列,性能怪兽
deque(double-ended queue)在两端插入/删除的时间复杂度都是 O(1) ,而 list 在头部操作是 O(n)。
python
from collections import deque
# ✅ 基础操作
d = deque([1, 2, 3])
d.appendleft(0) # O(1) ← 头部插入
d.append(4) # O(1) ← 尾部插入
print(d.popleft()) # 0, O(1) ← 头部弹出
print(d.pop()) # 4, O(1) ← 尾部弹出
# ✅ 滑动窗口(maxlen 参数极其有用)
window = deque(maxlen=3)
for i in range(5):
window.append(i)
print(list(window))
# [0]
# [0, 1]
# [0, 1, 2]
# [1, 2, 3] ← 自动淘汰最旧元素
# [2, 3, 4]
# ✅ 旋转操作
d = deque([1, 2, 3, 4, 5])
d.rotate(2) # 向右旋转2位
print(d) # deque([4, 5, 1, 2, 3])
d.rotate(-1) # 向左旋转1位
print(d) # deque([5, 1, 2, 3, 4])
🧪 动手验证:体验 list vs deque 的速度差异
下面两段代码请分别单独运行 (
N越大差异越明显;如果你的机器很快,可把N调到500_000或更大)。list:头部插入(慢)
pythonimport time N = 200_000 history = [] t0 = time.perf_counter() for i in range(N): history.insert(0, i) t1 = time.perf_counter() print("list.insert(0, x) 用时:", round(t1 - t0, 3), "秒")deque:头部插入(快)
pythonfrom collections import deque import time N = 200_000 history = deque() t0 = time.perf_counter() for i in range(N): history.appendleft(i) t1 = time.perf_counter() print("deque.appendleft(x) 用时:", round(t1 - t0, 3), "秒")
⚠️ 何时用
dequevslist?
- 只在尾部追加/弹出 → 用
list(常数因子更小)- 需要在头部频繁操作 / 需要固定大小滑动窗口 → 用
deque
🧪 动手验证 4 :用 deque(maxlen=5) 实现一个"最近5次搜索记录"的功能,连续添加8条记录,验证旧的记录是否被自动淘汰。
🔹 4. namedtuple:带字段名的元组
当你需要一个轻量级不可变对象,但又不想定义完整的类时,namedtuple 是完美选择。
python
from collections import namedtuple
# ✅ 定义一个"点"类型
Point = namedtuple("Point", ["x", "y", "z"])
p = Point(1, 2, 3)
# ✅ 像元组一样索引访问
print(p[0]) # 1
# ✅ 像对象一样用字段名访问(可读性大增)
print(p.x, p.y, p.z) # 1 2 3
# ✅ 转换为字典
print(p._asdict()) # {'x': 1, 'y': 2, 'z': 3}
# ✅ 替换字段值(返回新对象,原对象不变)
p2 = p._replace(y=10)
print(p2) # Point(x=1, y=10, z=3)
# ✅ 不可变(安全)
# p.x = 99 # AttributeError!
💡 典型用途:
- 数据库查询结果(替代元组,增加可读性)
- CSV 行解析
- 函数返回多个值时的结构化封装
🧪 动手验证 5 :创建一个 User = namedtuple("User", ["name", "age", "email"]),用 _asdict() 转成字典后,用 ** 解包传给一个函数。
🔹 5. ChainMap:多字典合并视图
ChainMap 将多个字典合并为一个逻辑视图,查找时按顺序搜索,修改只影响第一个字典。
python
from collections import ChainMap
# ✅ 配置优先级(最经典场景)
defaults = {"timeout": 30, "retry": 3, "debug": False}
user_config = {"timeout": 60, "verbose": True}
config = ChainMap(user_config, defaults)
# ✅ 查找:先查 user_config,找不到再查 defaults
print(config["timeout"]) # 60(用户配置优先)
print(config["retry"]) # 3(回退到默认值)
# ✅ 修改:只影响第一个字典
config["timeout"] = 120
print(user_config) # {'timeout': 120, 'verbose': True}
print(defaults) # {'timeout': 30, 'retry': 3, 'debug': False} ← 未被修改
# ✅ 新增键:只添加到第一个字典
config["new_key"] = "value"
print("new_key" in user_config) # True
print("new_key" in defaults) # False
🔑 核心价值 :
ChainMap是视图,不是深拷贝。修改原始字典会反映在 ChainMap 中,反之亦然。这让它非常适合实现"环境变量 > 用户配置 > 默认配置"的层级覆盖。
🧪 动手验证 6 :创建三个字典 env, user, default,用 ChainMap(env, user, default) 合并,验证查找优先级和修改隔离性。
🔹 6. OrderedDict:有序字典的额外能力
Python 3.7+ 的普通 dict 已经保证插入顺序,但 OrderedDict 提供了额外的方法:
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']
# ✅ 弹出最后一个/第一个
print(od.popitem()) # ('c', 3)
print(od.popitem(last=False)) # ('a', 1)
# ✅ 相等性比较:顺序敏感
d1 = OrderedDict(a=1, b=2)
d2 = OrderedDict(b=2, a=1)
print(d1 == d2) # False ← 普通 dict 比较是 True
💡 何时用
OrderedDict?
- 需要
move_to_end()/popitem(last=False)等顺序操作- 需要顺序敏感的相等性比较
- 需要兼容 Python 3.6 及以下版本
🧪 动手验证 7 :用 OrderedDict 实现一个简单的 LRU 缓存:每次访问一个键就 move_to_end(),缓存满时 popitem(last=False) 淘汰最久未使用的。