collections.defaultdict 是 Python 标准库 collections 模块中提供的一个字典增强版本 。它在普通字典(dict)的基础上,解决了一个非常常见的痛点:当访问字典中不存在的键(key)时,不需要手动判断键是否存在,而是会自动创建一个默认值。
这篇笔记将带你通俗易懂地了解 defaultdict 的核心概念、与普通字典的区别、常见应用场景以及实际开发中的使用技巧。
1. 核心概念与基本结构
在普通字典中,如果我们尝试访问或操作一个不存在的键,Python 会抛出 KeyError 报错。例如:
python
data = {}
data["a"].append(1) # 报错:KeyError: 'a'
而 defaultdict 通过接受一个默认工厂函数(default_factory)作为参数,巧妙地解决了这个问题。当你访问一个不存在的键时,它会自动调用这个工厂函数来生成一个默认值,并将其赋值给该键。
定义方式:
python
from collections import defaultdict
# default_factory 是一个可调用对象(如 int, list, set 或自定义函数)
d = defaultdict(default_factory)
工作原理示例:
python
from collections import defaultdict
# 使用 list 作为默认工厂函数
data = defaultdict(list)
# 访问不存在的键 "a" 时,自动调用 list() 生成空列表 [],然后执行 append(1)
data["a"].append(1)
print(data) # 输出:defaultdict(<class 'list'>, {'a': [1]})
2. defaultdict 与普通 dict 的对比
为了更直观地理解 defaultdict 的优势,我们来看一个经典的"统计单词出现次数"的例子。
普通 dict 写法
在使用普通字典时,我们需要在每次累加前,先判断键是否存在。如果不存在,则需要先初始化为 0。
python
words = ["apple", "banana", "apple"]
count = {}
for w in words:
if w not in count:
count[w] = 0 # 手动初始化
count[w] += 1
print(count) # 输出:{'apple': 2, 'banana': 1}
defaultdict 写法
使用 defaultdict,我们可以省去繁琐的判断和初始化步骤,代码更加简洁优雅。
python
from collections import defaultdict
words = ["apple", "banana", "apple"]
count = defaultdict(int) # 默认值为 int(),即 0
for w in words:
count[w] += 1 # 直接累加,不存在时自动初始化为 0
print(count) # 输出:defaultdict(<class 'int'>, {'apple': 2, 'banana': 1})
3. 常见参数类型与默认值
defaultdict 的行为取决于你传入的 default_factory。以下是几种最常用的参数类型及其默认值:
| 参数类型 | 默认值 | 适用场景 | 示例代码 |
|---|---|---|---|
int |
0 |
统计次数、计数器 | d = defaultdict(int); d["a"] += 5 |
list |
[] |
分组、收集同类数据、构建图结构 | d = defaultdict(list); d["a"].append(1) |
set |
set() |
去重聚合、收集唯一元素 | d = defaultdict(set); d["a"].add("AI") |
| 自定义函数 | 函数返回值 | 需要特定默认值时 | d = defaultdict(lambda: "未知") |
4. 经典应用场景
defaultdict 在实际开发中有着广泛的应用,特别适合处理以下几种场景:
场景一:数据分组(最经典用途)
当我们需要将一组数据按照某个特征进行分类时,defaultdict(list) 是最佳选择。
需求: 将学生按班级分组。
python
from collections import defaultdict
students = [
("A班", "张三"),
("B班", "李四"),
("A班", "王五")
]
classes = defaultdict(list)
for cls, name in students:
classes[cls].append(name)
print(dict(classes))
# 输出:{'A班': ['张三', '王五'], 'B班': ['李四']}
场景二:构建图结构
在算法中,图通常由节点和边组成。使用 defaultdict(list) 可以非常方便地构建邻接表。
python
from collections import defaultdict
edges = [("A", "B"), ("A", "C"), ("B", "D")]
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
print(dict(graph))
# 输出:{'A': ['B', 'C'], 'B': ['D']}
场景三:嵌套字典(多维数据)
当需要处理多层级的数据结构(如:用户 -> 月份 -> 订单)时,可以使用 lambda 结合 defaultdict 来实现自动嵌套。
python
from collections import defaultdict
# 创建一个二维字典:第一层默认值是另一个 defaultdict(list)
orders = defaultdict(lambda: defaultdict(list))
orders["张三"]["1月"].append("手机")
print(orders["张三"]["1月"]) # 输出:['手机']
5. 易混淆点:defaultdict vs dict.get()
很多人会将 defaultdict 和普通字典的 get() 方法混淆。它们虽然都能处理键不存在的情况,但行为有本质区别:
dict.get(key, default):仅仅是返回 一个默认值,不会在字典中真正创建这个键。defaultdict:在访问不存在的键时,不仅返回默认值,还会将该键和默认值实际存入字典中。
对比示例:
python
# dict.get() 的情况
d_normal = {}
print(d_normal.get("a", 0)) # 输出:0
print(d_normal) # 输出:{} (字典依然为空)
# defaultdict 的情况
from collections import defaultdict
d_default = defaultdict(int)
print(d_default["a"]) # 输出:0
print(d_default) # 输出:defaultdict(<class 'int'>, {'a': 0}) (键 'a' 已被创建)
6. 实际业务案例:API 调用统计
在 AI 系统或后台服务中,我们经常需要统计不同用户调用不同模型的次数。这是一个典型的二维统计需求。
数据与需求:
统计每个用户对各个模型的调用次数。
python
from collections import defaultdict
# 模拟日志数据
logs = [
("张三", "GPT"),
("李四", "Claude"),
("张三", "GPT"),
("张三", "Claude")
]
# 构建二维统计字典
stats = defaultdict(lambda: defaultdict(int))
for user, model in logs:
stats[user][model] += 1
# 转换为普通字典打印,方便查看
import json
print(json.dumps(stats, ensure_ascii=False, indent=4))
输出结果:
json
{
"张三": {
"GPT": 2,
"Claude": 1
},
"李四": {
"Claude": 1
}
}
这种结构在日志统计、权限系统、报表生成等场景中非常实用。
7. 注意事项
- 避免意外创建键 :因为
defaultdict在访问不存在的键时会自动创建它,所以在仅仅是"检查"或"读取"数据时要小心。如果只是想判断键是否存在,应该使用if key in d:,而不是直接读取d[key]。 - 美化输出 :直接打印
defaultdict时,输出会带有defaultdict(<class '...'>, ...)的前缀,不够美观。在最终输出或返回数据时,可以使用dict(d)将其转换为普通字典。
总结
defaultdict 就是一个**"不会因为键不存在而报错的增强版字典"**。它通过预设的工厂函数自动处理缺失键的初始化逻辑,极大地简化了代码。
核心记忆口诀:
- 统计次数 -> 用
defaultdict(int) - 分组/图结构 -> 用
defaultdict(list) - 去重聚合 -> 用
defaultdict(set) - 多维嵌套 -> 用
defaultdict(lambda: defaultdict(...))