news 2026/10/24 17:54:10

Python collections模块实例讲解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python collections模块实例讲解

前言


collections是标准库里最常被低估的模块之一。它的六个主力容器——Counter、defaultdict、deque、namedtuple、OrderedDict、ChainMap——各自解决一类手写代码既啰嗦又容易出错的问题。很多人用了一辈子 dict 加get来计数,却不知道Counter一行就能搞定,还自带most_common。


一个常见误解,是以为collections里的容器是「内置容器的高级版」,用哪个都行。事实恰恰相反:它们是专用工具,各自有明确的适用边界。defaultdict在in判断上会引入副作用;Counter访问不存在的键返回 0 而不报错;deque适合两端操作但随机访问慢;OrderedDict在普通 dict 已经保序之后,它的价值只剩「有序性参与相等比较」和move_to_end这几处了。


本文按容器逐个讲定位和典型用法,每个都给一段能直接复制运行的骨架。所有示例以 Python 3.8 及以上的写法为准。


一、Counter:一行做计数


Counter是dict的子类,专门用来统计可哈希对象出现的次数。它的值是计数,可以是任意整数,包括 0 和负数。


# 适用于 Python 3.8+
from collections import Counter

words = ["a", "b", "a", "c", "a", "b"]
c = Counter(words)
print(c) # Counter({'a': 3, 'b': 2, 'c': 1})
print(c["a"]) # 3
print(c["z"]) # 0,不存在的键返回 0,不抛 KeyError
print(c.most_common(2)) # [('a', 3), ('b', 2)]

# 从可迭代对象、映射、关键字参数三种来源构造
print(Counter("abracadabra").most_common(2)) # [('a', 5), ('b', 2)]
print(Counter({"red": 4, "blue": 2}))
print(Counter(cats=4, dogs=8))

# 计数可以原地加减,也可以做多重集运算
c.update(["a", "z"]) # 按数量累加
print(c["a"], c["z"]) # 4 1
c.subtract({"a": 1}) # 按数量扣减,允许出现 0 或负数
print(c["a"]) # 3

most_common(n)返回计数最高的 n 个键值对,按计数降序、同计数按首次出现顺序。不传参数时返回全部。元素运算里+是计数相加、-是相减且只保留正数、&取逐键最小值、|取逐键最大值。elements()会把每个元素按其计数重复产出,计数小于 1 的会被忽略。较新的 CPython 还提供了total(),用来求所有计数之和;如果你的运行环境里没有这个方法,等价写法是sum(c.values())。


二、defaultdict:消灭「不存在就初始化」


分组(把元素按某个特征归到不同的列表里)是最典型的场景。用普通 dict 写,每轮都要判断键在不在;defaultdict把这一步省掉:访问不存在的键时,它先调用default_factory生成默认值、写进字典,再返回。


# 适用于 Python 3.8+
from collections import defaultdict

rows = [("fruit", "apple"), ("veg", "carrot"), ("fruit", "pear")]

grouped = defaultdict(list)
for k, v in rows:
grouped[k].append(v) # 键不存在时自动得到空列表
print(dict(grouped)) # {'fruit': ['apple', 'pear'], 'veg': ['carrot']}

# default_factory 传 int 时,默认值是 0,天然适合累加
counter = defaultdict(int)
for k, _ in rows:
counter[k] += 1
print(dict(counter)) # {'fruit': 2, 'veg': 1}

# 传 set 时可以做去重分组
by_first = defaultdict(set)
for k, v in rows:
by_first[k].add(v[0])
print(dict(by_first)) # {'fruit': {'a', 'p'}, 'veg': {'c'}}

关键点:defaultdict的第一个参数是一个可调用对象(工厂函数),不是默认值本身。传int得到 0、传list得到空列表、传set得到空集合。传0或[]会抛TypeError,因为它们不可调用。不传参数时,缺失键的行为跟普通 dict 一样抛KeyError。


三、deque:两端都是 O(1)


deque(double-ended queue,双端队列)支持在两端高效地增减元素,两端的 append 和 pop 都是 O(1)。它还支持maxlen参数,满了以后从另一端自动丢弃,特别适合「只保留最近 N 条」的场景。


# 适用于 Python 3.8+
from collections import deque

d = deque([1, 2, 3])
d.append(4) # 右端加
d.appendleft(0) # 左端加
print(list(d)) # [0, 1, 2, 3, 4]
print(d.pop()) # 4,右端弹出
print(d.popleft()) # 0,左端弹出
d.rotate(1) # 整体右移一格
print(list(d)) # [3, 1, 2]

# maxlen:固定窗口,满了自动丢最旧的
recent = deque(maxlen=3)
for i in range(5):
recent.append(i)
print(list(recent)) # [2, 3, 4]

方案对比:用 list 当队列时,尾部 append 是均摊 O(1),但头部pop(0)要挪动全部剩余元素,是 O(n)。deque 的popleft()是 O(1)。deque 的代价在于随机访问:按下标取中间的元素会退化为 O(n),两端附近才是 O(1)。所以「频繁按下标读」要用 list,「频繁两端进出」才用 deque。


四、namedtuple:给元组字段起名


namedtuple生成一个 tuple 的子类,既有元组的不可变、可解包、可哈希,又能用obj.field按名字访问,比裸元组可读得多。


# 适用于 Python 3.8+
from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])
p = Point(3, 5)
print(p.x, p.y) # 3 5,按名字访问
print(p[0], p[1]) # 3 5,按下标访问
x, y = p # 依然可以解包
print(p == (3, 5)) # True,它就是元组

# _replace 返回新实例,不修改原对象
q = p._replace(x=10)
print(p, q) # Point(x=3, y=5) Point(x=10, y=5)
print(p._asdict()) # {'x': 3, 'y': 5}
print(Point._fields) # ('x', 'y')

# 带默认值(defaults 从最右边的字段开始生效)
Task = namedtuple("Task", ["name", "priority", "done"], defaults=[1, False])
print(Task("write")) # Task(name='write', priority=1, done=False)

字段名不能是 Python 关键字,也不能以下划线开头(避免与_fields、_replace这类自带名字冲突)。defaults关键字参数从 Python 3.7 起才有。


五、OrderedDict 与 ChainMap


OrderedDict是「记住顺序」的字典。在 Python 3.7 之后普通 dict 也保证插入顺序,所以它的独有价值收缩到两点:一是move_to_end(key, last=True)可以把某个键移到开头或末尾;二是它的相等比较是顺序敏感的——两个元素相同但顺序不同的 OrderedDict 不相等,而两个 dict 只要内容相同就相等。


# 适用于 Python 3.8+
from collections import OrderedDict, ChainMap

od = OrderedDict([("a", 1), ("b", 2), ("c", 3)])
od.move_to_end("a") # 把 a 挪到末尾
print(list(od.keys())) # ['b', 'c', 'a']
print(od.popitem(last=False)) # ('b', 2),从头弹出
print(OrderedDict([("a", 1), ("b", 2)]) == OrderedDict([("b", 2), ("a", 1)])) # False

# ChainMap:把多个映射串成一条查找链,从前到后找第一个命中
defaults = {"color": "black", "size": "M"}
user = {"color": "red"}
merged = ChainMap(user, defaults)
print(merged["color"]) # red,user 优先
print(merged["size"]) # M,回退到 defaults
print(list(merged)) # ['color', 'size']

ChainMap的写操作只作用于第一个映射,del也一样。它适合「多层配置叠加」:命令行参数 > 环境变量 > 默认值。它的new_child()返回一个在前端插入新空映射的新 ChainMap,parents返回去掉第一层的视图。




容器一句话定位别用它来



Counter计数与多重集运算存非计数类的值

defaultdict分组、累加,免去初始化判断需要「不写入」的探测式访问

deque两端进出的队列 / 固定窗口频繁随机下标访问

namedtuple轻量不可变记录需要频繁改字段

OrderedDict需要 move_to_end 或顺序敏感比较只需要普通保序

ChainMap多层配置叠加查找需要合并成一份真实数据



常见坑点



  1. 用in去探测 defaultdict,反而把键写进去了。


❌if key in dd:之后又dd[key]——看似无事,但如果换成直接dd[key]探测,就会在字典里凭空多出这个键。


✅ 只想探测就老实用dd.get(key)或if key in dd,别用下标访问做「试探」。



  1. 给 defaultdict 传了默认值而不是工厂。


❌defaultdict([])—— 抛TypeError: first argument must be callable or None。


✅ 传类型本身:defaultdict(list)、defaultdict(set)、defaultdict(int)。



  1. 以为 Counter 访问不存在的键会报错。


❌ 依赖KeyError来判断某元素是否出现过——Counter返回的是 0,不会抛异常。


✅ 判断「出现过没有」用if w in counter,或者看counter[w] > 0。



  1. 以为把计数设为 0 就删掉了元素。


❌c["x"] = 0之后"x" in c仍是True,遍历时它还在。


✅ 要真正移除用del c["x"],或c = +c(一元加号会把非正计数的项丢掉)。



  1. 用 deque 频繁按下标取中间元素。


❌d[1000]在长 deque 上是 O(n),看着像 list 用法其实慢得多。


✅ 需要随机访问就用 list;deque 只在两端进出时才有优势。



  1. namedtuple 的字段名不合法。


❌namedtuple("T", ["class", "1st"])—— 关键字和数字开头都不允许,抛ValueError。


✅ 字段名用合法标识符,需要「避开关键字」时常见做法是加下划线,如class_。



  1. 以为 ChainMap 写进去的会落到所有层。


❌merged["size"] = "L"只改了user这一层,defaults纹丝不动。


✅ 想真正合并成一份数据,先dict(merged);想改某一层就显式操作那一层的映射。



  1. 以为 OrderedDict 和 dict 完全等价。


❌ 拿两个顺序不同的 OrderedDict 做==,期待像 dict 一样相等——结果是False。


✅ 需要「顺序不参与比较」就用普通 dict;需要顺序敏感比较才用 OrderedDict。


总结




需求推荐容器关键方法



统计频次Countermost_common/elements/total

分组、累加defaultdictdefault_factory

队列、双端队列dequeappendleft/popleft/maxlen

带字段名的记录namedtuple_replace/_asdict/_fields

需要重排顺序OrderedDictmove_to_end/popitem(last=...)

多层配置查找ChainMapnew_child/parents



collections的价值在于「把常见模式固化成一个有名字的类」。判断该不该用它,只看一条:这段手写逻辑是不是反复出现、是不是容易写错。是的话,标准库大概率已经替你封装好了。





版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 17:53:31

python 2.7.14安装图文教程

前言 先说一个必须摆在最前面的事实:Python 2.7 已于 2020 年 1 月 1 日停止维护(依据是官方的 PEP 373 生命周期文档,其中明确写着「Support officially stopped January 1 2020」)。也就是说,Python 2.7 系列不再收到…

作者头像 李华
网站建设 2026/10/24 10:31:16

python 特殊属性及方法详细解析

前言 「特殊属性」和「特殊方法」这两个词经常被混着用,其实它们在语言参考里是两套不同的东西。特殊方法(也叫魔术方法)是那些会被语法隐式调用的、可调用的下划线方法,比如 __init__、__len__;特殊属性是那些由解释器…

作者头像 李华
网站建设 2026/10/24 14:18:09

Dify项目DSL实战:把财务报销审核助手做成可复用的工程资产

简介:这是一份面向企业财务合规与流程自动化场景的Dify工作流应用资源,支持直接导入项目DSL,聚焦报销单据审核中的规则校验、缺失字段补全、风险等级划分与汇总表输出,适合财务人员、Dify开发者及企业IT运维者直接参考或二次开发。…

作者头像 李华
网站建设 2026/10/22 6:36:21

TPS259483AYWPR与MKV58F1M0VLQ24协同电源保护设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 20:51:54

高校后勤报修系统设计与实现:毕设选题、开发到答辩全攻略

每年四月中旬开始,我手里就会集中收到一批“卡在中期”的毕设项目,出现频率最高的题目之一就是高校后勤报修系统。它看起来非常规整——提交报修、后台派单、师傅维修、学生验收,好像四个页面一套 CRUD 就能交差。可真把它做完,你…

作者头像 李华
网站建设 2026/10/21 4:00:23

Agent优化别靠玄学:从日志评测到分层调参的工程化方法

你的Agent又不听话了?先别急着改提示词。这是很多团队的日常:Agent效果不好,第一反应就是把系统提示词拿出来改改。加一句"你必须调用工具",不行;改成"请务必使用工具",还是不行&#…

作者头像 李华