
前言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.8from collections import Counterwords [a, b, a, c, a, b]c Counter(words)print(c) # Counter({a: 3, b: 2, c: 1})print(c[a]) # 3print(c[z]) # 0不存在的键返回 0不抛 KeyErrorprint(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(cats4, dogs8))# 计数可以原地加减也可以做多重集运算c.update([a, z]) # 按数量累加print(c[a], c[z]) # 4 1c.subtract({a: 1}) # 按数量扣减允许出现 0 或负数print(c[a]) # 3most_common(n)返回计数最高的 n 个键值对按计数降序、同计数按首次出现顺序。不传参数时返回全部。元素运算里是计数相加、-是相减且只保留正数、取逐键最小值、|取逐键最大值。elements()会把每个元素按其计数重复产出计数小于 1 的会被忽略。较新的 CPython 还提供了total()用来求所有计数之和如果你的运行环境里没有这个方法等价写法是sum(c.values())。二、defaultdict消灭「不存在就初始化」分组把元素按某个特征归到不同的列表里是最典型的场景。用普通 dict 写每轮都要判断键在不在defaultdict把这一步省掉访问不存在的键时它先调用default_factory生成默认值、写进字典再返回。# 适用于 Python 3.8from collections import defaultdictrows [(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] 1print(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)dequedouble-ended queue双端队列支持在两端高效地增减元素两端的 append 和 pop 都是 O(1)。它还支持maxlen参数满了以后从另一端自动丢弃特别适合「只保留最近 N 条」的场景。# 适用于 Python 3.8from collections import dequed 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(maxlen3)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.8from collections import namedtuplePoint 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(x10)print(p, q) # Point(x3, y5) Point(x10, y5)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(namewrite, priority1, doneFalse)字段名不能是 Python 关键字也不能以下划线开头避免与_fields、_replace这类自带名字冲突。defaults关键字参数从 Python 3.7 起才有。五、OrderedDict 与 ChainMapOrderedDict是「记住顺序」的字典。在 Python 3.7 之后普通 dict 也保证插入顺序所以它的独有价值收缩到两点一是move_to_end(key, lastTrue)可以把某个键移到开头或末尾二是它的相等比较是顺序敏感的——两个元素相同但顺序不同的 OrderedDict 不相等而两个 dict 只要内容相同就相等。# 适用于 Python 3.8from collections import OrderedDict, ChainMapod OrderedDict([(a, 1), (b, 2), (c, 3)])od.move_to_end(a) # 把 a 挪到末尾print(list(od.keys())) # [b, c, a]print(od.popitem(lastFalse)) # (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]) # reduser 优先print(merged[size]) # M回退到 defaultsprint(list(merged)) # [color, size]ChainMap的写操作只作用于第一个映射del也一样。它适合「多层配置叠加」命令行参数 环境变量 默认值。它的new_child()返回一个在前端插入新空映射的新 ChainMapparents返回去掉第一层的视图。容器一句话定位别用它来Counter计数与多重集运算存非计数类的值defaultdict分组、累加免去初始化判断需要「不写入」的探测式访问deque两端进出的队列 / 固定窗口频繁随机下标访问namedtuple轻量不可变记录需要频繁改字段OrderedDict需要 move_to_end 或顺序敏感比较只需要普通保序ChainMap多层配置叠加查找需要合并成一份真实数据常见坑点用in去探测 defaultdict反而把键写进去了。❌if key in dd:之后又dd[key]——看似无事但如果换成直接dd[key]探测就会在字典里凭空多出这个键。✅ 只想探测就老实用dd.get(key)或if key in dd别用下标访问做「试探」。给 defaultdict 传了默认值而不是工厂。❌defaultdict([])—— 抛TypeError: first argument must be callable or None。✅ 传类型本身defaultdict(list)、defaultdict(set)、defaultdict(int)。以为 Counter 访问不存在的键会报错。❌ 依赖KeyError来判断某元素是否出现过——Counter返回的是 0不会抛异常。✅ 判断「出现过没有」用if w in counter或者看counter[w] 0。以为把计数设为 0 就删掉了元素。❌c[x] 0之后x in c仍是True遍历时它还在。✅ 要真正移除用del c[x]或c c一元加号会把非正计数的项丢掉。用 deque 频繁按下标取中间元素。❌d[1000]在长 deque 上是 O(n)看着像 list 用法其实慢得多。✅ 需要随机访问就用 listdeque 只在两端进出时才有优势。namedtuple 的字段名不合法。❌namedtuple(T, [class, 1st])—— 关键字和数字开头都不允许抛ValueError。✅ 字段名用合法标识符需要「避开关键字」时常见做法是加下划线如class_。以为 ChainMap 写进去的会落到所有层。❌merged[size] L只改了user这一层defaults纹丝不动。✅ 想真正合并成一份数据先dict(merged)想改某一层就显式操作那一层的映射。以为 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/parentscollections的价值在于「把常见模式固化成一个有名字的类」。判断该不该用它只看一条这段手写逻辑是不是反复出现、是不是容易写错。是的话标准库大概率已经替你封装好了。