「Python 进阶之路」系列 Day28
写在前面
collections 模块里有一批 dict/tuple 的"增强版",几乎每个 Python 项目都会用到。今天把最常用的四个------defaultdict、Counter、OrderedDict、namedtuple------一次讲透,重点回答一个经常被问到的疑问:Python 3.7 后 dict 已经有序了,OrderedDict 是不是已经没用了?
一、是什么:dict 和 tuple 的增强版工具箱
collections 模块提供了一批 dict/tuple 的增强版,各自针对一种常见的使用痛点做了专门优化:
二、为什么:各自解决了什么手写代码的痛点
- 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:专门用来计数
Counter 是 dict 的子类,专门用来计数,最大的便利是访问不存在的 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_cache、partial、wraps 这几个装饰器/工具函数到底解决了什么问题。