python collections
## 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说起。假设你要统计一个列表中字符出现的次数,用普通 dict:
data=['a','b','a','c','b','a']count={}forchindata:ifchnotincount:count[ch]=0count[ch]+=1换成defaultdict:
fromcollectionsimportdefaultdict count=defaultdict(int)forchindata:count[ch]+=1当一个键不存在时,defaultdict会调用int()返回 0,代码可读性提升很多。
Counter把这件事又简化了一层:
fromcollectionsimportCounter count=Counter(data)# 一行搞定Counter还提供了most_common(n)的方法,直接取出现次数最多的前 n 个元素,这在分析日志、词频统计时非常有用。
再说namedtuple。假设你在处理地理坐标:
fromcollectionsimportnamedtuple Point=namedtuple('Point',['x','y'])p=Point(10,20)print(p.x)# 10print(p[0])# 10它既可以像 tuple 一样按索引访问,也可以像对象一样按属性访问。这种方式在函数返回多个值时尤其有用,比普通 tuple 更自文档化,也比写一个完整的类更轻量。
deque的两端操作性能与 list 完全不同。list 在头部插入是 O(n),deque 是 O(1)。当你需要实现一个固定长度的滑动窗口或历史记录时:
fromcollectionsimportdeque history=deque(maxlen=5)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.cache=OrderedDict()self.capacity=capacitydefget(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(last=False)最佳实践
实际项目中,用defaultdict而不是多次setdefault能显著减少缩进层级。如果你发现一段代码里有三个以上的if key in dict判断,大概率能改用defaultdict或Counter重写。
namedtuple在替代大量元组解包时效果很好。比如数据库查询返回的每一行,如果用一个 namedtuple 来表示,比用 dict 更节省内存,而且属性访问比索引访问更安全。不过要注意,namedtuple 是不可变的,要修改就得新建一个_replace。
deque的场景相对集中。如果你处理的是 IO 缓冲区、消息队列、或者需要按顺序处理任务,deque比 list 更合适。但如果你只是做简单的 append / pop,list 就已经够用了。
对于Counter,它不仅仅能计数。两个 Counter 可以直接做加减运算,这在处理语料库或流量统计时非常方便。比如统计两天的访问数据:
day1=Counter({'a':3,'b':2})day2=Counter({'a':1,'b':4,'c':2})total=day1+day2# {'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 标准库中最实用的“磨刀不误砍柴工”例子。它不炫技,不华丽,但只要用对了地方,它总能让你的代码变得干净、容易维护,还能跑得快一些。
