Python 的 defaultdict 是什么?和普通 dict + setdefault 有什么区别?
简化版
**collections.defaultdict 是 dict 的子类,它在「访问一个不存在的键」时会自动调用你提供的「工厂函数」生成一个默认值、并把这个键存进去,从而避免 KeyError——常用来省掉「先判断键在不在、不在就初始化」的样板代码。**用法:defaultdict(工厂函数),工厂函数是一个「无参可调用对象」,决定默认值——defaultdict(list)(默认值 [],用于分组)、defaultdict(int)(默认 0,用于计数)、defaultdict(set)(默认空集合)、defaultdict(lambda: "N/A")(自定义默认值)。最典型场景:① 分组——d[key].append(item) 直接用,不用先建空列表;② 计数——d[word] += 1 直接累加(初始 0)。和 dict.setdefault 的区别:setdefault(key, default) 每次调用都会构造 default(即使键已存在也会算一遍 default),而 defaultdict 只在缺键时才调工厂;defaultdict 更省、更简洁。关键坑:defaultdict「一访问缺键就自动创建键」——即使只是读取/判断(d[k]),也会悄悄插入一个默认值(用 in 或 .get 判断存在性,别用 d[k])。核心记忆:defaultdict(工厂)缺键时自动生成默认值,分组用 list、计数用 int;但读缺键也会插入键(判断用 in/get)。
详细版
defaultdict 的用法:
| 工厂 | 默认值 | 典型用途 |
|---|---|---|
defaultdict(list) | [] | 分组 d[k].append(v) |
defaultdict(int) | 0 | 计数 d[k] += 1 |
defaultdict(set) | set() | 去重分组 d[k].add(v) |
defaultdict(dict) | {} | 嵌套字典 |
defaultdict(lambda: x) | x | 自定义默认值 |
from collections import defaultdict
# 分组(最典型):不用先判断键是否存在
words = ["apple", "banana", "avocado", "cherry", "blueberry"]
groups = defaultdict(list)
for w in words:
groups[w[0]].append(w) # 首字母不存在也能直接 append
# {'a': ['apple','avocado'], 'b': ['banana','blueberry'], 'c': ['cherry']}
# 计数
counts = defaultdict(int)
for c in "mississippi":
counts[c] += 1 # 初始为 0,直接累加
# {'m':1, 'i':4, 's':4, 'p':2}
# 对比普通 dict 的啰嗦写法
groups2 = {}
for w in words:
if w[0] not in groups2: # 要先判断+初始化
groups2[w[0]] = []
groups2[w[0]].append(w)
# 对比 setdefault(可行但每次都构造默认值)
groups3 = {}
for w in words:
groups3.setdefault(w[0], []).append(w) # 每次都新建 [] 再丢弃
# ⚠️ 坑:读缺键也会插入键
d = defaultdict(int)
x = d["missing"] # 读了不存在的键
print(dict(d)) # {'missing': 0} —— 键被自动创建了!
⚠️ defaultdict 的核心机制是重写了 dict 的
__missing__方法——当你用d[key]访问一个不存在的键时,普通 dict 抛KeyError,而 defaultdict 会调用__missing__,后者用你传入的default_factory(工厂函数)生成一个默认值、把它存进d[key]、然后返回它。这带来两个要点:① 只有d[key]这种「下标访问」才触发——d.get(key)、key in d不会触发(它们不走__missing__);② 一旦触发就会「真的插入」这个键(不只是返回默认值),所以「仅仅读一下不存在的键」会产生副作用(字典里多了个键)。这个「读也会插入」的行为常导致隐蔽 bug:循环里if d[k] > 0这类判断会把所有检查过的键都塞进字典。想「只查不建」就用key in d或d.get(key, default)。另外工厂函数必须是「无参可调用」(list、int、lambda: ...),传defaultdict(list())(带括号,传的是空列表不是函数)是错的。
完整版教学
一、defaultdict 是什么
先理解 defaultdict 的定位:
collections.defaultdict:dict 的子类
多了一个 default_factory(默认值工厂函数)
→ 访问缺失的键时,自动用工厂生成默认值并插入
构造:
defaultdict(default_factory)
default_factory 是"无参可调用对象":
list、int、set、dict、str、float、
自定义函数、lambda: 值
行为差异(vs 普通 dict):
普通 dict:d[缺失键] → KeyError
defaultdict:d[缺失键] → 调工厂生成默认值、插入、返回
例子:
d = defaultdict(int)
d["a"] # "a" 不存在 → int() = 0 → 存 d["a"]=0 → 返回 0
d = defaultdict(list)
d["x"] # → list() = [] → 存 → 返回 []
解决的痛点:
省掉"先判断键在不在、不在就初始化"的样板代码
从:if k not in d: d[k]=[]; d[k].append(v)
到:d[k].append(v) (defaultdict(list))
除了缺键行为,其余和 dict 一样:
支持所有 dict 操作(in、get、items、update...)
isinstance(d, dict) → True
所以 defaultdict 是 dict 子类,缺键时用工厂自动生成默认值,省初始化样板
collections.defaultdict 是 dict 的子类,多了一个 default_factory(默认值工厂函数)——访问缺失的键时自动用工厂生成默认值并插入。构造:defaultdict(default_factory),工厂是「无参可调用对象」(list、int、set、dict、str、自定义函数、lambda)。行为差异(vs 普通 dict):普通 dict d[缺失键] → KeyError、defaultdict d[缺失键] → 调工厂生成默认值、插入、返回。解决的痛点:省掉「先判断键在不在、不在就初始化」的样板代码(从 if k not in d: d[k]=[] 到直接 d[k].append(v))。其余和 dict 一样(支持所有 dict 操作、isinstance(d, dict) 为 True)。理解「defaultdict 是 dict 子类、多 default_factory;缺键时调工厂生成默认值+插入+返回(dict 会 KeyError);省初始化样板;其余和 dict 一样」,就理解了 defaultdict 是什么。
二、missing 机制
理解 defaultdict 背后的 __missing__ 原理:
defaultdict 的原理:重写 __missing__ 方法
dict 的下标访问 d[key] 流程:
① 键存在 → 返回值
② 键不存在 → 调用 d.__missing__(key)
- 普通 dict:__missing__ 未定义 → 抛 KeyError
- defaultdict:__missing__ 用 default_factory 生成值
defaultdict.__missing__(key) 做的事:
if self.default_factory is None:
raise KeyError(key) # 没设工厂时仍 KeyError
value = self.default_factory() # 调工厂(无参)
self[key] = value # ★ 插入这个键!
return value # 返回默认值
关键点1:只有 d[key] 下标访问触发 __missing__
d[key] → 缺键触发 __missing__(自动创建)
d.get(key) → 不触发(get 有自己的逻辑,返回 None/默认)
key in d → 不触发(只查存在性)
关键点2:触发后"真的插入"键(副作用)
d[key] 读缺键 → 字典里多了 key(见后面的坑)
default_factory=None:
defaultdict(None) 等价普通 dict(缺键仍 KeyError)
d.default_factory 可读可改
所以 defaultdict 靠重写__missing__:缺键时(仅 d[key])调工厂生成+插入键+返回
defaultdict 的原理:重写 __missing__ 方法。dict 的下标访问 d[key] 流程:① 键存在返回值、② 键不存在调用 d.__missing__(key)(普通 dict 未定义 __missing__ → 抛 KeyError、defaultdict 用 default_factory 生成值)。defaultdict.__missing__(key) 做的事:若 default_factory 为 None 则 raise KeyError、否则 value = default_factory()(调工厂无参)、self[key] = value(★插入这个键)、return value。关键点1:只有 d[key] 下标访问触发 __missing__(d.get(key)、key in d 不触发)。关键点2:触发后「真的插入」键(副作用)。defaultdict(None) 等价普通 dict(缺键仍 KeyError)。理解「defaultdict 靠重写__missing__:缺键时(仅 d[key]下标访问)调工厂生成默认值+插入键(副作用)+返回;get/in 不触发;factory=None 时仍 KeyError」,就掌握了 __missing__ 机制。
三、典型场景:分组与计数
理解 defaultdict 最常用的两个场景:
场景1:分组(defaultdict(list))
把元素按某个 key 归类到列表
from collections import defaultdict
groups = defaultdict(list)
for item in items:
groups[key_of(item)].append(item)
# key 不存在时自动建 [],直接 append
例:按首字母分组单词、按部门分组员工、按状态分组订单
场景2:计数(defaultdict(int))
统计每个 key 出现次数
counts = defaultdict(int)
for x in data:
counts[x] += 1 # 初始 0,直接 +1
# 等价 Counter(但 Counter 更专业,见 Counter 题)
场景3:去重分组(defaultdict(set))
groups = defaultdict(set)
for user, tag in data:
groups[user].add(tag) # 自动去重
场景4:嵌套结构
# 二维计数 d[a][b] += 1
d = defaultdict(lambda: defaultdict(int))
d["x"]["y"] += 1
# 树形结构(无限嵌套)
tree = lambda: defaultdict(tree)
root = defaultdict(tree)
root["a"]["b"]["c"] # 任意深度自动建
对比手写:
defaultdict 版比 if-not-in-then-init 版短一半、更清晰
所以典型场景:分组(list)、计数(int)、去重分组(set)、嵌套(lambda:defaultdict)
场景1:分组(defaultdict(list))——把元素按某个 key 归类到列表:groups[key_of(item)].append(item)(key 不存在时自动建 []);用于按首字母分组单词、按部门分组员工。场景2:计数(defaultdict(int))——统计出现次数:counts[x] += 1(初始 0 直接 +1,等价 Counter 但 Counter 更专业)。场景3:去重分组(defaultdict(set)):groups[user].add(tag)(自动去重)。场景4:嵌套结构:二维计数 defaultdict(lambda: defaultdict(int))、树形结构 tree = lambda: defaultdict(tree)(任意深度自动建)。对比手写:defaultdict 版比 if-not-in-then-init 版短一半、更清晰。理解「典型场景:分组(list)d[k].append、计数(int)d[k]+=1、去重分组(set)d[k].add、嵌套(lambda:defaultdict(int)二维/tree 树形);比手写初始化短更清晰」,就掌握了典型场景。
四、vs setdefault vs dict.get
理解 defaultdict 和其他方案的区别:
三种「缺键给默认值」的方案对比:
① defaultdict(factory):
d[k].append(v) # 缺键自动建,简洁
优点:最简洁、只在缺键时调工厂(省)
缺点:读缺键也会插入键(副作用);类型是 defaultdict
② dict.setdefault(k, default):
d.setdefault(k, []).append(v)
行为:k 存在返回现有值、不存在则设为 default 并返回
缺点:★每次调用都构造 default(即使 k 已存在!)
setdefault(k, []) # 每次都新建 [] 再决定用不用 → 浪费
setdefault(k, expensive()) # expensive 每次都算!
优点:普通 dict,不改变类型;不会因读而插入(它本就是"设")
③ dict.get(k, default):
x = d.get(k, 0) # 只读取,不修改字典
行为:k 存在返回值、不存在返回 default(不插入)
用途:只读默认值(不需要写回)
→ 判断/读取用它,不会污染字典
选择:
频繁写入+分组/计数 → defaultdict(最省最简洁)
偶尔用+不想改字典类型 → setdefault
只读默认值+不改字典 → get
关键区别(setdefault 的坑):
setdefault 的 default 参数每次都求值(哪怕键存在)
defaultdict 的 factory 只在缺键时才调用
→ 默认值构造昂贵时,defaultdict 明显更优
所以 defaultdict 缺键才调工厂(省)、setdefault 每次构造 default、get 只读不改
三种「缺键给默认值」方案对比:① defaultdict(factory):d[k].append(v) 最简洁、只在缺键时调工厂(省),缺点读缺键也插入键、类型是 defaultdict;② dict.setdefault(k, default):k 存在返回现有值、不存在设为 default 并返回,缺点**★每次调用都构造 default(即使 k 已存在)**(setdefault(k, expensive()) 每次都算),优点普通 dict 不改类型;③ dict.get(k, default):只读取不修改字典(k 不存在返回 default 不插入),用于只读默认值。选择:频繁写入+分组/计数用 defaultdict、偶尔用+不想改类型用 setdefault、只读默认值用 get。关键区别(setdefault 的坑):setdefault 的 default 每次都求值、defaultdict 的 factory 只在缺键时调用(默认值构造昂贵时 defaultdict 明显更优)。理解「三方案:defaultdict(缺键才调工厂最省、读缺键会插入)、setdefault(每次都构造 default 即使键存在-坑、不改字典类型)、get(只读不改字典);频繁写用 defaultdict、只读用 get」,就掌握了三者区别。
五、读缺键会插入键的坑
理解 defaultdict 最隐蔽的坑:
坑:defaultdict 读一个不存在的键,会自动创建它
d = defaultdict(list)
print(d["missing"]) # 打印 [](看似只是读)
print(dict(d)) # {'missing': []} —— 键被创建了!
print(len(d)) # 1(多了一个键)
为什么:d[key] 触发 __missing__,__missing__ 会 self[key]=value
常见踩坑:
① 用 d[k] 做判断/读取:
if d[user]: # 只是想判断 user 有没有数据
...
→ 把所有判断过的 user 都塞进了字典(内存/数据污染)
② 遍历中意外访问:
for k in some_keys:
total += d[k] # 每个 k 都被创建(即使原来没有)
正确做法(只查不建):
用 in 判断存在性:
if key in d: ...
用 get 读取(不插入):
val = d.get(key, default)
if d.get(user): ...
遍历已有键:
for k in list(d): # 遍历现有键
调试提示:
发现 defaultdict 莫名多出很多空值键([] / 0)
→ 多半是某处用 d[k] 读了缺键
所以坑:读缺键(d[k])会自动插入键(污染字典),只查不建用 in 或 get
坑:defaultdict 读一个不存在的键,会自动创建它——d["missing"] 打印 [](看似只是读),但 dict(d) 变成 {'missing': []}(键被创建了、len(d) 变 1)。为什么:d[key] 触发 __missing__、__missing__ 会 self[key]=value。常见踩坑:① 用 d[k] 做判断/读取(if d[user] 想判断有没有数据、却把所有判断过的 user 塞进字典、内存/数据污染)、② 遍历中意外访问(total += d[k] 每个 k 都被创建)。正确做法(只查不建):用 in 判断存在性、用 d.get(key, default) 读取(不插入)、遍历已有键 for k in list(d)。调试提示:发现 defaultdict 莫名多出很多空值键([]/0),多半是某处用 d[k] 读了缺键。理解「坑:读缺键 d[k]会自动插入键(污染字典、len 变大);踩坑 if d[user]判断/total+=d[k]遍历;只查不建用 in 判断、get 读取、遍历用 list(d)」,就掌握了这个隐蔽的坑。
六、注意事项与总结
总结 defaultdict 的实践:
注意事项:
① 工厂必须是"无参可调用":
✓ defaultdict(list)、defaultdict(int)、defaultdict(lambda: "x")
✗ defaultdict(list()) # 传的是空列表[](不可调用)→ 报错
✗ defaultdict([1,2]) # 同样错
② 读缺键会插入键 → 判断用 in、读取用 get
③ 类型是 defaultdict:
- dict(d) 可转回普通 dict
- 序列化/传递前可能想转 dict(有的库不认 defaultdict)
④ 用完可"冻结":转成普通 dict 防止后续误插入
d.default_factory = None # 之后缺键恢复 KeyError
defaultdict vs Counter:
计数场景 Counter 更专业(most_common、加减运算、见 Counter 题)
defaultdict(int) 是通用的计数手段
→ 纯计数用 Counter、计数+其他逻辑用 defaultdict(int)
核心总结:
defaultdict(工厂):缺键时自动生成默认值并插入
分组用 list、计数用 int、去重用 set、嵌套用 lambda
原理:重写 __missing__(仅 d[key] 触发、会插入键)
vs setdefault:defaultdict 只缺键才调工厂(省)
坑:读缺键也插入(判断用 in/get)
工厂要"无参可调用"(别加括号)
所以 defaultdict(工厂)缺键自动建,分组/计数首选,工厂无参可调用,读缺键会插入
注意事项:① 工厂必须是「无参可调用」(defaultdict(list) 对、defaultdict(list()) 传的是空列表不可调用会报错);② 读缺键会插入键(判断用 in、读取用 get);③ 类型是 defaultdict(dict(d) 可转回普通 dict、序列化前可能想转);④ 用完可「冻结」(d.default_factory = None 之后缺键恢复 KeyError)。defaultdict vs Counter:纯计数用 Counter 更专业、计数+其他逻辑用 defaultdict(int)。理解「注意:工厂无参可调用(别加括号)、读缺键会插入(用 in/get)、dict(d)转回、可 default_factory=None 冻结;纯计数用 Counter、通用计数 defaultdict(int)」,就掌握了注意事项与总结。
记忆钩子:「collections.defaultdict 是 dict 的子类,访问『不存在的键』时自动调用你传入的『工厂函数』生成默认值并插入,避免 KeyError、省掉『先判断键在不在再初始化』的样板;用法 defaultdict(工厂):list→默认[]用于分组 d[k].append(v)、int→默认 0 用于计数 d[k]+=1、set→默认空集去重分组 d[k].add(v)、lambda:x→自定义默认值;★原理:重写了 dict 的__missing__方法——只有 d[key]下标访问缺键才触发(d.get/key in d 不触发),触发后会『真的插入』这个键;★最隐蔽的坑:读一个不存在的键(d[k])也会自动创建它(污染字典、len 变大),所以判断存在性用 key in d、读取用 d.get(k,default)、别用 d[k];vs setdefault:setdefault(k,default)每次调用都构造 default(即使键已存在,expensive()每次都算),defaultdict 的工厂只在缺键时才调用(更省);注意工厂必须『无参可调用』(defaultdict(list)对、defaultdict(list())错——传的是空列表)」。
七、常见误区与追问
- 误区:defaultdict 和普通 dict 完全一样,只是名字不同。 不同——defaultdict 是 dict 的子类,多了一个
default_factory(工厂函数),核心区别在「访问不存在的键」时:普通 dict 抛KeyError,而 defaultdict 会自动调用工厂函数生成一个默认值、插入这个键、并返回;其余操作(in、get、items 等)都和 dict 一样,isinstance(d, dict)也为 True。 - 误区:defaultdict(list()) 和 defaultdict(list) 一样。 完全不同——
defaultdict(list)传的是 list 这个「类型/可调用对象」(工厂函数,defaultdict 缺键时会调用list()生成空列表),是正确的;defaultdict(list())传的是list()即一个空列表[](不是可调用对象),会报错「first argument must be callable or None」;工厂参数必须是「无参可调用」的东西,别加括号。 - 误区:用 d[key] 读取 defaultdict 只是读、不会改变字典。 会改变——
d[key]访问不存在的键时会触发__missing__,它不仅返回默认值、还会把这个键真正插入字典;所以if d[user]:这类「只想判断」的写法会把所有检查过的键都塞进字典、造成数据/内存污染;想「只查不建」要用key in d(判断存在性)或d.get(key, default)(读取但不插入)。 - 误区:defaultdict 和 setdefault 效果一样、随便用哪个。 有性能差异——
dict.setdefault(k, default)的 default 参数每次调用都会被求值构造(即使键已经存在、这个 default 会被丢弃),所以setdefault(k, [])每次都新建一个空列表、setdefault(k, expensive())每次都执行 expensive();而 defaultdict 的工厂函数只在「键确实不存在」时才被调用;频繁操作或默认值构造昂贵时,defaultdict 明显更高效、也更简洁。 - 追问:defaultdict 底层是怎么实现「缺键自动生成默认值」的? 靠重写 dict 的
__missing__方法:当用d[key]下标访问一个不存在的键时,dict 的__getitem__会去调用__missing__(key)——普通 dict 没有实现这个方法所以抛 KeyError,而 defaultdict 实现了它:如果default_factory是 None 就仍抛 KeyError,否则调用default_factory()(无参)生成默认值、执行self[key] = value把键插入、再返回这个值;注意只有d[key]这种下标访问才走__missing__,d.get(key)和key in d都不会触发(所以它们不会插入键)。 - 追问:defaultdict 怎么做多层嵌套(比如二维计数或树形结构)? 二维计数用
d = defaultdict(lambda: defaultdict(int)),这样d[a][b] += 1时,外层缺 a 就自动建一个defaultdict(int)、内层缺 b 就自动给 0,直接累加;无限深的树形结构可以用递归定义tree = lambda: defaultdict(tree); root = defaultdict(tree),之后root["a"]["b"]["c"]会按需在任意深度自动创建节点;注意工厂必须是无参可调用,所以嵌套时用 lambda 包一层(lambda: defaultdict(int)),不能直接写defaultdict(defaultdict(int))(那样内层的 defaultdict(int) 是实例不是工厂)。 - 追问:defaultdict 和 Counter 都能计数,怎么选? 纯计数场景优先用
collections.Counter(也是 dict 子类):它专为计数设计、Counter(iterable)一行统计、most_common(n)取最高频、支持+/-/&/|等计数运算、访问不存在的键返回 0 且不插入键(比 defaultdict 更安全);defaultdict(int)是更通用的计数手段,适合「计数的同时还要做别的逻辑」、或需要自定义默认值/结构的场景;简单说:只是数个数用 Counter,计数逻辑复杂或要和其他默认值结构混用就用 defaultdict。
八、加强记忆
collections.defaultdict 是 dict 的子类,访问「不存在的键」时自动调用你传入的「工厂函数」生成默认值并插入,避免 KeyError、省掉「先判断键在不在再初始化」的样板。用法 defaultdict(工厂):list→默认 [](分组 d[k].append(v))、int→默认 0(计数 d[k] += 1)、set→默认空集(去重分组 d[k].add(v))、lambda: x→自定义默认值。原理:重写了 dict 的 __missing__ 方法——只有 d[key] 下标访问缺键才触发(d.get、key in d 不触发),触发后会「真的插入」这个键。最隐蔽的坑:读一个不存在的键(d[k])也会自动创建它(污染字典、len 变大),所以判断存在性用 key in d、读取用 d.get(k, default)、别用 d[k]。vs setdefault:setdefault(k, default) 每次调用都构造 default(即使键已存在),defaultdict 的工厂只在缺键时才调用(更省)。注意工厂必须「无参可调用」(defaultdict(list) 对、defaultdict(list()) 错——传的是空列表)。一句话「defaultdict(工厂)缺键时自动生成默认值并插入(分组用 list、计数用 int、去重用 set);靠重写__missing__(仅 d[key]触发、会插入键);读缺键也会插入是坑(判断用 in、读取用 get);比 setdefault 省(只缺键才调工厂);工厂要无参可调用别加括号」。