手写"判断key存不存在"的模板代码太烦?一文讲透 collections 四件套

「Python 进阶之路」系列 Day28

写在前面

collections 模块里有一批 dict/tuple 的"增强版",几乎每个 Python 项目都会用到。今天把最常用的四个------defaultdictCounterOrderedDictnamedtuple------一次讲透,重点回答一个经常被问到的疑问:Python 3.7 后 dict 已经有序了,OrderedDict 是不是已经没用了?


一、是什么:dict 和 tuple 的增强版工具箱

collections 模块提供了一批 dict/tuple 的增强版,各自针对一种常见的使用痛点做了专门优化:

flowchart LR D[dict] --> DD[defaultdict<br/>自动创建默认值] D --> C[Counter<br/>专门用来计数] D --> OD[OrderedDict<br/>顺序敏感的相等比较] T[tuple] --> NT[namedtuple<br/>按名字访问]

二、为什么:各自解决了什么手写代码的痛点

  • defaultdict:省掉手写"判断 key 是否存在,不存在就先初始化"这种模板代码
  • Counter:省掉手写"计数前先判断 key 存不存在"的模板代码,还顺带提供排序、数学运算这些计数场景的常用操作
  • OrderedDict:在 dict 已经天然有序的今天,它解决的是"顺序本身要不要参与相等比较、要不要支持手动调整顺序"这类更细粒度的需求
  • namedtuple:在"用索引访问可读性差的裸 tuple"和"写一个完整类太重"之间找一个折中

三、怎么用

1. defaultdict:省掉判断key存不存在的模板代码

python 复制代码
from collections import defaultdict

d = defaultdict(list)
d["fruits"].append("apple")   # 不需要先判断key存不存在
d["fruits"].append("banana")
print(dict(d))   # {'fruits': ['apple', 'banana']}

normal_d = {}
normal_d["fruits"].append("apple")
# KeyError: 'fruits'   ------ 普通dict必须先手动判断/初始化

最典型的应用场景是分组统计:

python 复制代码
students = [("Tom", "A"), ("Jerry", "B"), ("Alice", "A"), ("Bob", "C"), ("Eve", "B")]
groups = defaultdict(list)
for name, grade in students:
    groups[grade].append(name)
print(dict(groups))
# {'A': ['Tom', 'Alice'], 'B': ['Jerry', 'Eve'], 'C': ['Bob']}

2. Counter:专门用来计数

Counterdict 的子类,专门用来计数,最大的便利是访问不存在的 key 会返回 0,而不是报错

python 复制代码
from collections import Counter

c = Counter("abracadabra")
print(c)          # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
print(c["z"])       # 0,不报错!这是Counter和普通dict的关键区别

配合 most_common() 方法可以直接拿到出现次数最多的前 N 项:

python 复制代码
words = "the quick brown fox jumps over the lazy dog the fox runs".split()
word_count = Counter(words)
print(word_count.most_common(3))
# [('the', 3), ('fox', 2), ('quick', 1)]

Counter 还支持直接做数学运算:

python 复制代码
c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2)
print(c1 + c2)   # Counter({'a': 4, 'b': 3})
print(c1 - c2)   # Counter({'a': 2}) ------ 相减结果为负数或0的会被丢弃

3. OrderedDict:3.7后还有存在的意义吗

Day26 讲过,Python 3.7 起普通 dict 已经保证插入顺序了,那 OrderedDict 是不是已经过时?实测发现它还有两个普通 dict 没有的能力:

move_to_end() 方法(普通 dict 没有):

python 复制代码
from collections import OrderedDict

od = OrderedDict()
od["a"] = 1
od["b"] = 2
od["c"] = 3
od.move_to_end("a")
print(list(od.keys()))   # ['b', 'c', 'a']

d1 = {}
d1.move_to_end("a")
# AttributeError: 'dict' object has no attribute 'move_to_end'

这个方法常用来实现 LRU 缓存------每次访问一个 key 就把它挪到末尾,最久没被访问的自然留在开头,方便淘汰。

相等比较会考虑顺序(普通 dict 不会):

python 复制代码
od1 = OrderedDict([("a", 1), ("b", 2)])
od2 = OrderedDict([("b", 2), ("a", 1)])
print(od1 == od2)   # False ------ OrderedDict比较相等时会考虑顺序

d2a = {"a": 1, "b": 2}
d2b = {"b": 2, "a": 1}
print(d2a == d2b)     # True ------ 普通dict比较相等不考虑顺序,只看键值对

所以 OrderedDict 并没有过时:普通 dict 只是"顺序稳定"(能保证遍历顺序),但语义上依然把顺序当成"无关紧要的实现细节";OrderedDict 把顺序当成这个对象身份的一部分,需要"顺序本身也参与判断"的场景(LRU 缓存、需要严格比较两个有序结构是否完全一致)依然要用它。

4. namedtuple:给tuple的每个位置起名字

普通 tuple 用索引访问可读性差(point[0] 不知道是 x 还是 y),写一个完整的类又太重(要手写 __init__/__repr__/__eq__),namedtuple 是这两者之间的折中:

python 复制代码
from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])
p = Point(1, 2)
print(p.x, p.y)              # 1 2 ------ 属性访问,可读性好
print(p[0], p[1])              # 1 2 ------ 依然支持索引访问
print(isinstance(p, tuple))     # True ------ namedtuple确实是tuple的子类
x, y = p                        # 支持解包
print(x, y)                      # 1 2

p.x = 100
# AttributeError: can't set attribute ------ 依然不可变,和普通tuple一样

更现代的写法是用 typing.NamedTuple,用类型注解语法定义字段,可读性更好、还能配合类型检查工具:

python 复制代码
from typing import NamedTuple

class Point2(NamedTuple):
    x: int
    y: int

p2 = Point2(3, 4)
print(p2, p2.x, isinstance(p2, tuple))
# Point2(x=3, y=4) 3 True

四、面试追问

Q1:defaultdict 解决了什么问题?

访问不存在的 key 时自动用工厂函数创建一个默认值,而不是抛 KeyError,省掉手写"判断 key 是否存在再初始化"的模板代码,最典型的应用场景是分组统计。

Q2:Counter 和普通 dict 计数相比有什么优势?

访问不存在的 key 直接返回 0 而不报错,可以直接写 counter[key] += 1 而不用先判断;此外还提供 most_common() 方法快速取出现次数最多的项,并支持 +/- 这类数学运算直接合并/相减多个计数结果。

Q3:Python 3.7 后 dict 已经有序了,OrderedDict 还有存在的意义吗?

有。OrderedDict 提供了 move_to_end() 方法(普通 dict 没有,常用于实现 LRU 缓存),并且它的相等比较会把顺序也纳入判断(普通 dict 比较相等时不考虑顺序,只看键值对本身),这两点是普通 dict 做不到的。

Q4:namedtuple 解决了什么问题,它和普通类相比有什么取舍?

解决了"普通 tuple 用索引访问可读性差、写完整类又太重"这个折中问题,用命名字段访问的同时保留了 tuple 的轻量、不可变、可解包特性。取舍是它不能像普通类那样自由添加方法或可变状态,适合表示简单的、不可变的数据结构。

Q5:这几个 collections 工具的共同设计思路是什么?

都是针对 dict/tuple 某个特定使用场景的痛点做专门优化,而不是重新发明一套通用数据结构:defaultdict 解决默认值问题、Counter 解决计数问题、OrderedDict 解决顺序敏感问题、namedtuple 解决可读性问题,各自只专注做好一件事。


下一篇预告

Day29 讲 functools 模块------lru_cachepartialwraps 这几个装饰器/工具函数到底解决了什么问题。

相关推荐
2601_9623815811 分钟前
[Python人工智能] 九.gensim词向量Word2Vec安装及《庆余年》中文短文本相似度计算
人工智能·python·tensorflow·word2vec·文本相似度
️学习的小王14 分钟前
Flask实现云栖笔记|开箱即用网页版本地笔记本
笔记·python·flask
quantdash_cc20 分钟前
批量请求和循环请求有什么区别?从 API 请求次数看量化数据获取的工程设计
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
寻求出路的程序媛21 分钟前
分布式 & 高性能 & 高可用 体系、学习重点、面试点
分布式·后端·面试·性能优化
码农刚子22 分钟前
告别影楼和付费 App,5 分钟本地搭一个证件照自由平台|HivisionIDPhotos 开箱实测
python·图像识别
掘金者阿豪32 分钟前
Let‘s Encrypt 证书到底会不会自动续期?从一次服务器迁移后的证书排查说起
后端
Tizzy JJ37 分钟前
Python + pytest 接口自动化测试框架实战:从零搭建企业级项目骨架
开发语言·python·pytest
海兰38 分钟前
【 Python 量化交易】第8章:量化工具箱
开发语言·python
joinwell5239 分钟前
Agent 中断后,原任务如何安全接管?从恢复标记到效果事实
人工智能·后端·架构