## Python collections 模块那些事Python 这门语言很多人一开始觉得它简单写着写着却会发现有些东西就像那句“看似寻常最奇崛”。collections 模块就是这种存在。它不是语言的基础部分但它在标准库中占据了一个特别的位置——如果应用得当它能把写代码这件事从“代码能跑”变成“代码让人觉得舒服”。它是什么collections 是 Python 标准库中的一个模块专门用来扩展内置的数据类型。内置的那些 list、dict、tuple 在大部分场景下够用但总有一些特殊情况比如你要计数、要维护有顺序的字典、要创建轻量级的数据容器。这些时候如果只靠内置类型去硬拼代码很快就会变得冗长且难以维护。collections 本质上是一组容器数据类型的集合它们不是代替内建类型而是在内建类型的基础上提供更精确的工具。好比修车扳手和螺丝刀当然够用但遇到某些特殊螺丝有专用工具能省很多力气。它能做什么用一个生活中的场景去理解也许会容易一些。假设你在管理一个图书馆读者可以借书、还书、预约。这个过程中会遇到很多情况记录每本书被借出的次数。普通 dict 能记录但每次更新都要判断键是否存在、值是否为 None。Counter就是为这种事准备的直接加或者初始化都方便。需要记住读者预约的顺序同时又能快速删除某条记录。如果只用 dict顺序就丢了如果只用 list查找速度又慢。OrderedDict保留了插入顺序兼顾了查找效率和顺序。希望默认情况下取一个不存在的键时不报错而是返回一个默认值。普通的 dict 得用setdefault或者defaultdict看着就麻烦。defaultdict直接帮你处理了这种事。如果你想要一个堆栈和队列的结合体允许两边都可以高效地添加或弹出元素。deque就是专门干这个的。还有一种场景想定义一个轻量级的、不可变的数据结构就像 C 语言的结构体那样但又同时具备 tuple 的不可变性和 dict 的键访问能力。namedtuple正合适。当你定义复杂的类既要实现属性访问又要保证不可变或者要自定义散列逻辑。UserDict、UserList、UserString这三个东西很少被提及但它们是继承自定义行为的好帮手。坦白说很多开发者多年使用 Python 也不一定认识UserDict但其他几个几乎每天都在用。怎么使用先从最常用的defaultdict说起。假设你要统计一个列表中字符出现的次数用普通 dictdata[a,b,a,c,b,a]count{}forchindata:ifchnotincount:count[ch]0count[ch]1换成defaultdictfromcollectionsimportdefaultdict countdefaultdict(int)forchindata:count[ch]1当一个键不存在时defaultdict会调用int()返回 0代码可读性提升很多。Counter把这件事又简化了一层fromcollectionsimportCounter countCounter(data)# 一行搞定Counter还提供了most_common(n)的方法直接取出现次数最多的前 n 个元素这在分析日志、词频统计时非常有用。再说namedtuple。假设你在处理地理坐标fromcollectionsimportnamedtuple Pointnamedtuple(Point,[x,y])pPoint(10,20)print(p.x)# 10print(p[0])# 10它既可以像 tuple 一样按索引访问也可以像对象一样按属性访问。这种方式在函数返回多个值时尤其有用比普通 tuple 更自文档化也比写一个完整的类更轻量。deque的两端操作性能与 list 完全不同。list 在头部插入是 O(n)deque 是 O(1)。当你需要实现一个固定长度的滑动窗口或历史记录时fromcollectionsimportdeque historydeque(maxlen5)history.append(a)history.append(b)history.append(c)history.append(d)history.append(e)history.append(f)# 此时 a 自动被移除maxlen参数自动淘汰旧元素省去了手动判断长度并删除元素的麻烦。OrderedDict在 Python 3.7 之后确实失去了部分光芒因为普通 dict 也保留了插入顺序。但它仍然有用武之地——当你需要move_to_end这样的方法时普通 dict 做不到。比如实现一个 LRU最近最少使用缓存OrderedDict是天然的选择fromcollectionsimportOrderedDictclassLRUCache:def__init__(self,capacity):self.cacheOrderedDict()self.capacitycapacitydefget(self,key):ifkeynotinself.cache:return-1self.cache.move_to_end(key)returnself.cache[key]defput(self,key,value):ifkeyinself.cache:self.cache.move_to_end(key)self.cache[key]valueiflen(self.cache)self.capacity:self.cache.popitem(lastFalse)最佳实践实际项目中用defaultdict而不是多次setdefault能显著减少缩进层级。如果你发现一段代码里有三个以上的if key in dict判断大概率能改用defaultdict或Counter重写。namedtuple在替代大量元组解包时效果很好。比如数据库查询返回的每一行如果用一个 namedtuple 来表示比用 dict 更节省内存而且属性访问比索引访问更安全。不过要注意namedtuple 是不可变的要修改就得新建一个_replace。deque的场景相对集中。如果你处理的是 IO 缓冲区、消息队列、或者需要按顺序处理任务deque比 list 更合适。但如果你只是做简单的 append / poplist 就已经够用了。对于Counter它不仅仅能计数。两个 Counter 可以直接做加减运算这在处理语料库或流量统计时非常方便。比如统计两天的访问数据day1Counter({a:3,b:2})day2Counter({a:1,b:4,c:2})totalday1day2# {a: 4, b: 6, c: 2}OrderedDict的用途在大多数情况下可以被普通 dict 取代。只有当需要move_to_end或popitem这种操作时它才有不可替代性。另外推荐一个冷门但实用的组合ChainMap。当你需要把多个字典合并成一个逻辑整体同时又不希望破坏原始字典时可以用它。比如处理多层配置系统配置 - 用户配置 - 命令行参数ChainMap能按优先级查找。和同类技术对比如果不使用 collections能替代defaultdict、Counter这类工具的方式无非是写更多条件判断代码。这不算同类技术对比更像是“有工具 vs 纯手写”的比较。第三方库方面有时我会看到有人用pandas做简单计数 —— 杀鸡用牛刀。Pandas 的Series.value_counts()和Counter功能类似但加入 pandas 这个依赖库仅仅为了做计数并不划算。同样情况还出现在iteration_utilities、sortedcontainers等库中它们提供了更多的数据结构但对于基本需求collections 已经自给自足。真正与 collections 形成竞争者的是 Python 自身的发展。随着 Python 版本的演进一些 collections 的功能被内置类型吸收。前面说过Python 3.7 的 dict 保留了插入顺序。Python 3.10 中dataclasses的出现又让namedtuple显得更“朴素”一些 —— dataclass 提供了更多灵活性能定义方法、默认值、类型注解。但这并不意味着 collections 过时了。namedtuple创建的对象比 dataclass 更轻量内存占用更小代码也更简洁。如果只需要一个简单的、不可变的数据容器namedtuple仍然是最佳选择。在处理海量数据时collections 也提供了性能优势。比如用deque代替 list 做队列操作用Counter代替手写字典计数这些差距在数据规模小时不明显当数据量上升到百万级别时collections 的工具通常经过底层优化表现得更好。整体来说collections 模块是 Python 标准库中最实用的“磨刀不误砍柴工”例子。它不炫技不华丽但只要用对了地方它总能让你的代码变得干净、容易维护还能跑得快一些。