在 Python 编程中,处理字典(dict)时最常遇到的“烦恼”之一就是 KeyError。当尝试访问一个尚不存在的键时,Python 会立即抛出这个错误。

这在计数分组等场景下尤为不便,因为总是需要写下面这样的防御性代码:

# 繁琐的常规 dict 写法
counts = {}
key = 'some_key'

if key not in counts:
    counts[key] = 0  # 必须先手动初始化
counts[key] += 1

为了解决这个问题,Python 的 collections 模块提供了一个强大的子类:defaultdict

核心:defaultdict 为什么存在?

defaultdict 的核心功能是:在访问不存在的键时,它会自动调用一个“默认工厂函数”来创建并返回一个默认值,从而彻底避免 KeyError

它继承自 dict,所以它拥有常规字典的所有方法(如 .keys(), .values(), .items()),但它在初始化时多接受一个参数:

from collections import defaultdict

# 语法:defaultdict(default_factory)

这个 default_factory 是一个可调用对象(如函数、类型构造函数 listint,或 lambda 表达式),它会在需要时被调用,且不带任何参数


工作原理:魔法揭秘

当使用 defaultdict(factory) 创建一个字典 d 并执行 d[key] 时:

  1. Python 检查 key 是否已经在 d 中。

  2. 如果 key 存在defaultdict 的行为与常规 dict 完全相同,返回 d[key] 的值。

  3. 如果 key 不存在(魔法发生的地方):

    a. defaultdict 会调用提供的 default_factory(例如,int() 或 list())。

    b. default_factory 返回一个默认值(例如,0 或 [])。

    c. defaultdict 将这个默认值赋给 d[key]。

    d. 最后,它返回这个新创建的默认值。


三大经典用例

defaultdict 的美妙之处在于其简洁性。让我们来看看它最常见的三个用法。

1. 计数 (Counting): defaultdict(int)

这是 defaultdict 最著名的用法。当需要统计一系列项目中各项出现的次数时使用它。

  • 默认工厂: int

  • 工厂返回值: int() 会返回 0

示例:统计单词频率

from collections import defaultdict

sentence = "the quick brown fox jumps over the lazy dog"
word_counts = defaultdict(int) # 当键不存在时,调用 int() 返回 0

for word in sentence.split():
    # 如果 word 第一次出现 (如 'quick'):
    # 1. word_counts['quick'] 不存在
    # 2. 调用 int() -> 得到 0
    # 3. word_counts['quick'] 被设为 0
    # 4. 执行 0 + 1
    
    # 如果 word 已存在 (如 'the'):
    # 1. word_counts['the'] 存在 (值为 1)
    # 2. 执行 1 + 1
    
    word_counts[word] += 1

print(word_counts)
# 输出: defaultdict(<class 'int'>, 
# {'the': 2, 'quick': 1, 'brown': 1, 'fox': 1, 'jumps': 1, 'over': 1, 'lazy': 1, 'dog': 1})

对比常规 dict defaultdict(int) 省去了所有 if key not in ... 的检查。

提示:defaultdict(int) 这种计数功能,其实还有一个更专门的“近亲”叫作 Counter

2. 分组 (Grouping): defaultdict(list)

当需要将数据项按类别收集到列表中时使用它。

  • 默认工厂: list

  • 工厂返回值: list() 会返回一个空列表 []

示例:将学生按年级分组

from collections import defaultdict

students = [
    ('Alice', 'Seniors'),
    ('Bob', 'Juniors'),
    ('Charlie', 'Seniors'),
    ('David', 'Freshmen'),
    ('Eve', 'Juniors'),
]

# 按年级分组,值应该是 list
students_by_grade = defaultdict(list)

for name, grade in students:
    # 如果 grade 第一次出现 (如 'Seniors'):
    # 1. students_by_grade['Seniors'] 不存在
    # 2. 调用 list() -> 得到 []
    # 3. students_by_grade['Seniors'] 被设为 []
    # 4. 执行 [].append('Alice')
    
    students_by_grade[grade].append(name)

print(students_by_grade)
# 输出: defaultdict(<class 'list'>, 
# {'Seniors': ['Alice', 'Charlie'], 'Juniors': ['Bob', 'Eve'], 'Freshmen': ['David']})
3. 收集唯一项 (Grouping Unique Items): defaultdict(set)

defaultdict(list) 类似,但如果你只关心每个类别中有哪些唯一的成员(不关心顺序或重复),可以使用 set

  • 默认工厂: set

  • 工厂返回值: set() 会返回一个空集合 set()

示例:记录哪些用户访问了哪些页面

from collections import defaultdict

access_log = [
    ('/home', 'user1'),
    ('/login', 'user2'),
    ('/home', 'user3'),
    ('/home', 'user1'), # user1 重复访问
    ('/profile', 'user2'),
]

# 页面 -> 访问过的唯一用户
page_visitors = defaultdict(set)

for page, user in access_log:
    # 使用 .add() 而不是 .append()
    page_visitors[page].add(user) 

print(page_visitors)
# 输出: defaultdict(<class 'set'>, 
# {'/home': {'user1', 'user3'}, '/login': {'user2'}, '/profile': {'user2'}})
# 注意:/home 只有 {'user1', 'user3'},重复的 'user1' 被 set 自动处理了

🚀 高级技巧:lambda 与自定义工厂

default_factory 必须是一个不带参数的调用。如果你需要一个更复杂的默认值(比如一个固定的字符串或数字 1),可以使用 lambda 表达式。

from collections import defaultdict

# 用例1:默认值是一个固定字符串 "N/A"
d_str = defaultdict(lambda: "N/A")
print(f"访问 'key1': {d_str['key1']}")
print(f"字典内容: {dict(d_str)}")

# 用例2:默认值是 100 (而不是 0)
d_num = defaultdict(lambda: 100)
d_num['item_a'] += 5 # 第一次访问,100 + 5
print(f"访问 'item_a': {d_num['item_a']}")

输出:

访问 'key1': N/A
字典内容: {'key1': 'N/A'}
访问 'item_a': 105

总结

defaultdict 是 Python collections 库中的一颗明珠。

  • 何时使用? 当你需要计数、分组或聚合数据时,它几乎总是比常规 dict 更简洁、更易读。

  • 如何使用?

    • 计数: defaultdict(int) (默认值 0)

    • 分组: defaultdict(list) (默认值 [])

    • 收集唯一项: defaultdict(set) (默认值 set())

    • 自定义默认值: defaultdict(lambda: "some_value")

提醒: defaultdict 只是在键不存在时表现不同。一旦键被创建,它的所有行为(赋值、删除)都和普通 dict 一模一样。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐