第一次用defaultdict时,我正试图统计一段文本中每个单词出现的次数。当时我写了一个循环,用普通字典记录词频,结果遇到了烦人的KeyError——那些首次出现的单词根本不在字典里!就在我准备写一堆if key not in dict的判断时,同事指了指屏幕上的from collections import defaultdict。那一刻,我意识到Python标准库里藏着太多这样的"瑞士军刀"。
想象你正在整理一个图书馆。普通字典就像严格的图书管理员——如果你要借一本不存在的书,他会直接拒绝你。而defaultdict则是那位和蔼的管理员,当你询问一本未收录的书时,他会微笑着递给你一个空盒子:"这本书暂时没有,但你可以先拿着这个盒子,等找到书再放进去。"
用普通字典统计词频时,典型的"新手式"写法是这样的:
python复制text = "apple banana apple orange"
word_count = {}
for word in text.split():
if word not in word_count: # 必须检查key是否存在
word_count[word] = 0
word_count[word] += 1
这种模式在Python中被称为"检查然后设置"(Look Before You Leap)。而defaultdict允许我们采用更Pythonic的"请求宽恕比获得许可更容易"(Easier to Ask for Forgiveness than Permission)风格:
python复制from collections import defaultdict
word_count = defaultdict(int) # int()默认返回0
for word in text.split():
word_count[word] += 1 # 无需检查key是否存在
defaultdict的核心在于它的工厂函数(factory function)机制。当我们创建defaultdict(int)时:
int这个可调用对象int()作为默认值常见工厂函数及其默认值:
| 工厂函数 | 默认值 | 典型应用场景 |
|---|---|---|
list |
[] |
分组归类 |
int |
0 |
计数器/统计 |
set |
set() |
去重或关系网络 |
str |
'' |
字符串拼接 |
float |
0.0 |
科学计算 |
dict |
{} |
嵌套字典结构 |
提示:工厂函数也可以是自定义函数,比如
lambda: 'default'会为所有新key设置字符串'default'
defaultdict是dict的子类,它重写了__missing__方法。当访问不存在的key时:
python复制def __missing__(self, key):
if self.default_factory is None:
raise KeyError(key)
self[key] = value = self.default_factory() # 调用工厂函数
return value
这种设计带来了几个重要特性:
default_factory属性在IPython中用%timeit测试三种实现方式的性能:
python复制# 测试数据
data = [str(i) for i in range(10000)]
# 方法1:普通字典+判断
def method1():
d = {}
for key in data:
if key not in d:
d[key] = 0
d[key] += 1
# 方法2:setdefault
def method2():
d = {}
for key in data:
d.setdefault(key, 0)
d[key] += 1
# 方法3:defaultdict
def method3():
d = defaultdict(int)
for key in data:
d[key] += 1
测试结果(单位:毫秒):
| 方法 | 第一次运行 | 第二次运行 | 第三次运行 | 平均 |
|---|---|---|---|---|
| 普通字典 | 2.45 | 2.39 | 2.42 | 2.42 |
| setdefault | 3.21 | 3.18 | 3.25 | 3.21 |
| defaultdict | 1.87 | 1.83 | 1.85 | 1.85 |
从结果看,defaultdict比普通字典快约23%,比setdefault快约42%。这是因为:
setdefault需要额外的函数调用开销defaultdict只需一次哈希查找和内置方法调用在处理JSON-like的嵌套结构时,defaultdict能显著简化代码。比如构建城市-区域-街道的多级映射:
python复制cities = defaultdict(
lambda: defaultdict(
lambda: defaultdict(list)
)
)
# 添加数据
cities['北京']['朝阳区']['三里屯'].append('酒吧街')
cities['上海']['浦东新区']['陆家嘴'].append('金融中心')
# 查询不存在的路径会自动创建嵌套结构
print(cities['广州']['天河区']['珠江新城']) # 输出:[]
在实现图算法时,defaultdict能优雅地处理邻接表:
python复制graph = defaultdict(set) # 使用set避免重复边
# 添加边
edges = [('A', 'B'), ('B', 'C'), ('A', 'C'), ('C', 'D')]
for u, v in edges:
graph[u].add(v)
graph[v].add(u) # 无向图
# 查询邻居
print(graph['A']) # 输出:{'B', 'C'}
print(graph['X']) # 输出:set() (而不是KeyError)
在数据科学中,经常需要按某个维度分组:
python复制from collections import defaultdict
import pandas as pd
df = pd.DataFrame({
'department': ['销售', '技术', '销售', '财务', '技术'],
'salary': [8000, 12000, 8500, 9000, 11000]
})
dept_salaries = defaultdict(list)
for _, row in df.iterrows():
dept_salaries[row['department']].append(row['salary'])
# 计算各部门平均薪资
avg_salary = {dept: sum(sals)/len(sals)
for dept, sals in dept_salaries.items()}
工厂函数在每次访问缺失key时都会被调用,这有时会导致意外行为:
python复制d = defaultdict(list)
d['missing'].append(1) # 正常
d['missing'].append(2) # 继续使用已存在的列表
# 但如果是这样:
d = defaultdict(lambda: []) # 与list相同
d['missing'] += [1, 2] # 等价于extend
注意:
defaultdict的默认值不会出现在.keys()中,直到被显式访问
当需要将defaultdict序列化为JSON时:
python复制import json
d = defaultdict(int, a=1, b=2)
json.dumps(d) # 这能正常工作
json.dumps(d['c']) # 返回'0',但'd'中不会新增'c'
解决方案是在序列化前转换为普通字典:
python复制json.dumps(dict(d))
对于大型数据集,可以结合__missing__实现更智能的默认值:
python复制class SmartDefaultDict(defaultdict):
def __missing__(self, key):
if self.default_factory is None:
raise KeyError(key)
# 根据key生成特定默认值
value = self.default_factory(key)
self[key] = value
return value
# 使用示例
d = SmartDefaultDict(lambda k: f"default_for_{k}")
print(d['test']) # 输出:default_for_test
在数据分析项目中,我发现defaultdict最强大的地方在于处理不完整数据时能保持代码整洁。曾经处理过一个包含百万条记录的电商数据集,其中产品分类存在大量空缺。使用defaultdict(list)后,不仅避免了无数if-else判断,还让后续的MapReduce操作变得更加直观。