# Python Counter一个被低估的数据结构提到Python的collections模块很多人第一反应是defaultdict或者namedtupleCounter常常被当作“统计列表元素出现次数”的小工具。但深入了解后就会发现它远不止这么简单。它到底是什么Counter本质上是dict的子类专门设计用来计数的字典。它继承了dict的所有特性——哈希查找、可变、可迭代——同时添加了计数场景下的便捷方法。它的键是你要统计的元素值是该元素出现的次数。举个例子你有一篮子水果用Counter统计后得到的是“苹果3个香蕉2个橙子1个”这样的结构。它就像一个自动计数的记账本不需要手动维护数字的变化。有意思的是Counter对缺失键的处理很优雅如果你问“葡萄有多少个”它不会报错而是返回0。这一点让我觉得比普通dict更符合直觉——当你需要知道不存在的东西有多少时答案当然是零。它能做什么除了基础计数Counter的能力圈其实更大。它实现了集合运算可以用来做交集、并集、差集但不是对元素本身操作而是对计数进行操作。想象你在管理两个仓库的库存——第一个仓库有5支笔、3本书第二个仓库有2支笔、4本书。用Counter的交集运算可以得到“两个仓库都有的物品取较少数量”的结果就是2支笔。这听起来像什么对像是数据库里的join操作但用一行代码就能完成。它还能找出出现次数最多的元素。这个功能在文本分析中特别有用——统计文章中最常见的词、日志文件中出现频率最高的错误代码、电商订单中最火的商品。most_common方法带有内置排序时间复杂度是O(n log k)其中k是你想取的前几位数。如果只取一位效率会比全排序高不少。怎么使用创建fromcollectionsimportCounter# 从可迭代对象创建c1Counter([苹果,香蕉,苹果,橙子,香蕉,苹果])# 从字典创建c2Counter({苹果:3,香蕉:2,橙子:1})# 使用关键字参数c3Counter(苹果3,香蕉2)# 最常见的方式从字符串统计字符c4Counter(hello world)日常操作遍历一个Counter时默认遍历的是键而不是键值对这一点和普通dict不同cCounter(a3,b2,c1)foriteminc:print(item)# 只打印键如果同时需要键和值需要用items()方法这和普通dict一样。增删改给Counter增加计数很简单直接添加元素即可cCounter()c.update([a,b,a,c])# 现在c Counter({a: 2, b: 1, c: 1})删除元素可以用del也可以用subtract方法。但要注意subtract允许负值存在这有时会造成一些小陷阱——某个元素计数变成负值后你要么手动清理要么在获取时用操作符过滤掉负数。找前几位cCounter(abracadabra)print(c.most_common(3))# 输出 [(a, 5), (b, 2), (c, 2)]most_common不传参数时返回所有元素按次数降序排列。但如果Counter有大量不同元素这种全量排序可能会有点慢。最佳实践不要用Counter做高频计数。如果你需要在每秒百万次的循环中累加计数Counter的字典查找开销可能成为瓶颈。这时候用numpy数组或者简单的list索引会更高效。我曾经在一个实时日志处理场景中犯过这个错——Counter写起来漂亮但压测时发现CPU都在哈希表的扩容上。利用和-操作符合并计数器时注意边界。Counter的加法是把对应计数相加减法则会把结果截断到0以下——这和集合差集操作的行为一致但有时会误导新手。如果你需要计数不能为负可以这样做c1Counter(a1,b3)c2Counter(a2,b1)resultc1-c2# 结果是Counter({b: 2})a被去掉了因为1-20如果想让a的结果是0而不是被删除可以用字典推导式来转换。用Counter当作多集合。有些场景下Counter比set更好用。比如你需要检查两个列表是否有重叠元素不仅要知道是否重叠还要知道重叠了多少次。set只能给出布尔结果而Counter的操作能给出精确的交集计数。慎用Counter作为value的容器。有些人喜欢把Counter嵌套在字典里当统计工具用但这容易导致内存膨胀——每个嵌套的Counter都是一个独立对象。如果统计维度很多可以考虑用pandas的groupby或者SQL的聚合函数。和同类技术对比对比普通dict使用普通dict计数需要手动处理KeyErrord{}foriteminitems:ifitemind:d[item]1else:d[item]1Counter把这段样板代码压缩成一行Counter(items)。但这不只是语法糖Counter还额外提供了most_common、elements、subtract等实用方法以及集合操作。不足之处在于Counter引入了浮动开销。如果你的代码对内存敏感或者只需要最简单的计数比如只取最后统计结果普通dict配合defaultdict可能更快。对比defaultdictdefaultdict(int)也能做自动计数写起来类似fromcollectionsimportdefaultdict ddefaultdict(int)foriteminitems:d[item]1这个方法比Counter快因为defaultdict直接继承自dict没有Counter的额外方法。如果只需要计数功能defaultdict是更好的选择。但如果你需要most_common或集合运算就得自己实现了。对比numpy/pandas在数据量非常大时百万级以上Python原生的Counter和dict都会遇到性能瓶颈。numpy的bincount或者pandas的value_counts在大规模数值型数据上表现更好。pandas的value_counts甚至支持更复杂的操作比如按比例显示、排序方式、缺失值处理等。不过这些工具也有门槛——numpy只能处理整数类型pandas需要事先把数据加载到DataFrame中。如果是零散的小数据Counter的便携性完胜。简单总结一下Counter最擅长的是中小规模数据的快速统计需要同时兼顾计数和查询的场景以及需要集合运算的场合。性能敏感的地方用defaultdict规模大的地方用pandas高频操作还是用裸循环索引更稳。