Python 标准库里藏着不少"宝藏",collections 就是其中最值得深挖的一个。它不是什么花哨的第三方包,而是随 Python 一起安装、随时可用的内置模块——却偏偏被大量开发者长期忽视,习惯性地用普通 dict、list 硬撑所有场景。这篇文章就来系统梳理 collections 里的每一个数据类型,聊聊它们的设计逻辑、使用姿势,以及真正适合它们发光发热的场景。
一、namedtuple — 给元组起个名字,世界清晰多了
普通元组的问题人人都遇到过:point[0] 到底是 x 还是 y?三个月后回来看代码,完全不知道自己在写什么。namedtuple 就是为了解决这个痛点而生的。
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(3, 5)
print(p.x, p.y) # 3 5
print(p[0]) # 3,依然支持索引访问
它本质上是元组的子类,不可变、内存紧凑,却拥有字段名访问的可读性。和普通 class 相比,它不需要写 __init__,也不需要 __repr__,天生就是"数据容器"的最佳形态。
核心特性
_asdict()方法可以直接转成OrderedDict(Python 3.8+ 返回普通 dict)_replace()返回一个修改了某字段的新实例,原对象不变_fields属性列出所有字段名- 支持设置
defaults参数,给字段加默认值
Employee = namedtuple('Employee', ['name', 'dept', 'salary'], defaults=[50000])
e = Employee('Alice', 'Engineering')
print(e.salary) # 50000
适用场景
最典型的用途是替代轻量级数据类。比如解析 CSV 文件的每一行、表示数据库查询结果的一条记录、封装坐标/颜色/配置参数等。当你发现自己在写 return (x, y, z) 然后调用方要靠注释才能理解每个位置的含义时,就是换 namedtuple 的时候了。
Python 3.7 之后 dataclass 崛起,提供了可变性和更多功能,但 namedtuple 的不可变性和极低内存开销在某些场景下依然是无可替代的优势。
二、deque — 双端队列,列表的高性能替代品
list 在尾部追加元素很快,但在头部插入或删除,时间复杂度是 ——因为所有元素都要往后移。数据量一大,这个代价就很明显了。deque(发音 "deck")用双向链表结构解决了这个问题,两端操作都是 。
from collections import deque
dq = deque([1, 2, 3])
dq.appendleft(0) # 左端追加
dq.append(4) # 右端追加
dq.popleft() # 左端弹出
dq.pop() # 右端弹出
print(dq) # deque([1, 2, 3])
maxlen 参数:自动滚动的固定窗口
这是 deque 最迷人的特性之一。设置 maxlen 后,队列满了再追加新元素,旧元素会自动从另一端被挤出去,完全不需要手动管理。
recent = deque(maxlen=5)
for i in range(10):
recent.append(i)
print(recent) # deque([5, 6, 7, 8, 9], maxlen=5)
适用场景
- 实现队列(BFS)和栈:比用
list模拟队列性能好得多 - 滑动窗口:日志系统保留最近 N 条记录、实时数据流的移动平均
- 撤销/重做功能:用
maxlen限制历史记录条数 - 生产者-消费者模型:配合
rotate()方法做循环缓冲区
# rotate 示例:向右旋转
dq = deque([1, 2, 3, 4, 5])
dq.rotate(2)
print(dq) # deque([4, 5, 1, 2, 3])
三、Counter — 计数这件事,它是专业的
统计词频、计算元素出现次数——这类需求几乎每个项目都会遇到。手写一个 for 循环加 if key in dict 当然能实现,但 Counter 让这件事优雅得多。
from collections import Counter
words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
c = Counter(words)
print(c) # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
直接传入任何可迭代对象,甚至字符串,立刻得到计数结果。
那些让人惊喜的方法
most_common(n) 返回出现频率最高的 n 个元素,底层用堆实现,效率很高:
print(c.most_common(2)) # [('apple', 3), ('banana', 2)]
Counter 之间可以直接做算术运算,这个特性相当强大:
c1 = Counter({'a': 3, 'b': 2})
c2 = Counter({'a': 1, 'b': 4, 'c': 1})
print(c1 + c2) # Counter({'b': 6, 'a': 4, 'c': 1})
print(c1 - c2) # Counter({'a': 2}) # 负数结果被丢弃
print(c1 & c2) # Counter({'a': 1, 'b': 2}) # 取最小值(交集)
print(c1 | c2) # Counter({'b': 4, 'a': 3, 'c': 1}) # 取最大值(并集)
elements() 方法将计数展开成迭代器:
list(Counter({'a': 2, 'b': 3}).elements())
# ['a', 'a', 'b', 'b', 'b']
适用场景
- NLP 文本分析:词频统计、词云数据准备
- 数据去重计数:日志分析、用户行为统计
- 投票/排名系统:快速找出最热门的 N 项
- 差异比较:两个文本之间的词汇差异
- 字谜检测:判断两个字符串是否是字母异位词,
Counter(s1) == Counter(s2)一行搞定
四、defaultdict — 再也不用写 if key not in dict
写过这样的代码吗?
# 传统写法,繁琐
result = {}
for word in words:
if word not in result:
result[word] = []
result[word].append(something)
defaultdict 把这个模式彻底简化了。它在访问不存在的键时,自动调用你指定的工厂函数创建默认值,不会抛出 KeyError。
from collections import defaultdict
result = defaultdict(list)
for word in words:
result[word].append(something) # 直接用,不需要判断
工厂函数可以是任何可调用对象:
dd_int = defaultdict(int) # 默认值 0
dd_list = defaultdict(list) # 默认值 []
dd_set = defaultdict(set) # 默认值 set()
dd_str = defaultdict(str) # 默认值 ''
# 也可以用 lambda 自定义
dd_custom = defaultdict(lambda: 'N/A')
构建嵌套结构
defaultdict 在构建多层嵌套字典时尤其好用:
# 构建图的邻接表
graph = defaultdict(list)
edges = [('A', 'B'), ('A', 'C'), ('B', 'D')]
for u, v in edges:
graph[u].append(v)
# 二维分组统计
sales = defaultdict(lambda: defaultdict(int))
sales['Q1']['北京'] += 100
sales['Q1']['上海'] += 200
适用场景
- 分组聚合:按某字段将数据分组,比
groupby更灵活 - 图算法:构建邻接表是最经典的用法
- 词频统计的另一种写法:
defaultdict(int)配合+=1 - 缓存/记忆化:配合
lambda做简单的懒加载
五、OrderedDict — 有序字典的历史使命与现代价值
Python 3.7 之后,普通 dict 已经按插入顺序保存键了,这让很多人觉得 OrderedDict 已经过时。但它依然有几个独特的能力,是普通 dict 给不了的。
from collections import OrderedDict
od = OrderedDict()
od['a'] = 1
od['b'] = 2
od['c'] = 3
move_to_end() 方法
这是 OrderedDict 独有的杀手锏:
od.move_to_end('a') # 把 'a' 移到末尾
od.move_to_end('c', last=False) # 把 'c' 移到开头
相等性比较的差异
普通 dict 的相等性只看键值对是否一致,不管顺序;OrderedDict 则顺序不同就不相等:
d1 = {'a': 1, 'b': 2}
d2 = {'b': 2, 'a': 1}
print(d1 == d2) # True
od1 = OrderedDict([('a', 1), ('b', 2)])
od2 = OrderedDict([('b', 2), ('a', 1)])
print(od1 == od2) # False
适用场景
OrderedDict 最经典的应用是实现 LRU 缓存(最近最少使用缓存):
class LRUCache:
def __init__(self, capacity):
self.cache = OrderedDict()
self.capacity = capacity
def get(self, key):
if key not in self.cache:
return -1
self.cache.move_to_end(key) # 访问后移到末尾(最近使用)
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False) # 淘汰最久未使用的
当然,Python 3.2 之后 functools.lru_cache 装饰器已经内置了这个功能,但手动实现 LRU 时 OrderedDict 依然是最优雅的选择。
六、ChainMap — 多个字典,一个视图
ChainMap 把多个字典"链"在一起,形成一个逻辑上的单一映射。查找时按顺序逐个字典搜索,找到第一个匹配就返回;写入操作只作用于第一个字典。
from collections import ChainMap
defaults = {'color': 'red', 'user': 'guest', 'timeout': 30}
env_vars = {'user': 'admin', 'timeout': 60}
cli_args = {'color': 'blue'}
config = ChainMap(cli_args, env_vars, defaults)
print(config['color']) # 'blue'(来自 cli_args)
print(config['user']) # 'admin'(来自 env_vars)
print(config['timeout']) # 60(来自 env_vars)
这个优先级机制非常直观:越靠前的字典优先级越高。
与 dict.update() 的本质区别
用 {**defaults, **env_vars, **cli_args} 合并字典会创建一个全新的字典,原始数据的修改不会反映进来。ChainMap 是视图,原始字典的变化会实时体现:
config = ChainMap(cli_args, defaults)
defaults['timeout'] = 100
print(config['timeout']) # 100,实时更新
适用场景
- 配置系统:命令行参数 > 环境变量 > 配置文件 > 默认值,这种优先级层叠是
ChainMap的天然用途 - 模板引擎的变量作用域:局部变量覆盖全局变量
- Python 解释器内部:
locals()和globals()的关系就是类似的链式查找 - A/B 测试配置:不同用户群体使用不同配置层
七、UserDict、UserList、UserString — 继承的正确姿势
这三个类乍看之下有点奇怪——为什么要有 UserDict,直接继承 dict 不行吗?
答案是:直接继承内置类型有坑。dict 的某些方法(比如 __setitem__)在内部调用时不一定会走你重写的版本,导致行为不一致。UserDict 是用纯 Python 实现的字典包装器,内部所有方法都会正确地调用彼此,继承它来自定义字典行为更安全可靠。
from collections import UserDict
class UpperDict(UserDict):
"""键自动转大写的字典"""
def __setitem__(self, key, value):
super().__setitem__(key.upper(), value)
ud = UpperDict()
ud['hello'] = 'world'
print(ud) # {'HELLO': 'world'}
print(ud['HELLO']) # 'world'
同理,UserList 适合自定义列表行为:
from collections import UserList
class BoundedList(UserList):
"""有最大长度限制的列表"""
def __init__(self, max_size, *args):
self.max_size = max_size
super().__init__(*args)
def append(self, item):
if len(self.data) < self.max_size:
super().append(item)
else:
raise ValueError(f"列表已满,最多 {self.max_size} 个元素")
适用场景
- 自定义数据验证:插入时自动校验类型或范围
- 只读字典/列表:重写写入方法抛出异常
- 带日志的容器:每次修改自动记录日志
- 类型强制转换:插入时自动转换数据格式
八、全局视角:选哪个?
梳理完七个数据结构,用一张表来帮助快速定位:
| 数据类型 | 核心特性 | 最典型用途 | 性能亮点 |
|---|---|---|---|
namedtuple | 不可变、有字段名的元组 | 轻量数据类、函数多返回值 | 内存占用与元组相同 |
deque | 双端 操作 | 队列、滑动窗口、BFS | 两端操作远优于 list |
Counter | 自动计数 + 集合运算 | 词频、排名、差异比较 | most_common 用堆优化 |
defaultdict | 自动创建默认值 | 分组聚合、图邻接表 | 省去 KeyError 判断 |
OrderedDict | 有序 + move_to_end | LRU 缓存、顺序敏感比较 | move_to_end 是 |
ChainMap | 多字典链式查找视图 | 配置优先级、作用域链 | 无需复制数据 |
UserDict/List/String | 安全继承内置类型 | 自定义容器行为 | 继承语义完整可靠 |
写在最后
collections 的设计哲学其实很简单:为常见的编程模式提供专用工具。每一个数据结构背后都对应着一类反复出现的需求——计数、滑动窗口、分组、配置层叠……与其每次都从零开始用基础类型拼凑,不如直接用这些经过充分测试、性能优化的专用容器。
代码的质量不只体现在逻辑正确,还体现在用对了工具。看到 Counter 的时候不再手写计数循环,看到 deque 的时候不再用 list.insert(0, x)——这种直觉,是 Python 开发者成长路上很重要的一步。