collections.defaultdict 学习

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. 注意事项

  1. 避免意外创建键 :因为 defaultdict 在访问不存在的键时会自动创建它,所以在仅仅是"检查"或"读取"数据时要小心。如果只是想判断键是否存在,应该使用 if key in d:,而不是直接读取 d[key]
  2. 美化输出 :直接打印 defaultdict 时,输出会带有 defaultdict(<class '...'>, ...) 的前缀,不够美观。在最终输出或返回数据时,可以使用 dict(d) 将其转换为普通字典。

总结

defaultdict 就是一个**"不会因为键不存在而报错的增强版字典"**。它通过预设的工厂函数自动处理缺失键的初始化逻辑,极大地简化了代码。

核心记忆口诀:

  • 统计次数 -> 用 defaultdict(int)
  • 分组/图结构 -> 用 defaultdict(list)
  • 去重聚合 -> 用 defaultdict(set)
  • 多维嵌套 -> 用 defaultdict(lambda: defaultdict(...))