← 返回题目列表

Python 的 defaultdict 是什么?和普通 dict + setdefault 有什么区别?

高频 中等 第 8 / 21 题 更新于 2026/07/31
Pythondefaultdictcollections字典

简化版

**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 dd.get(key, default)。另外工厂函数必须是「无参可调用」(listintlambda: ...),传 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);③ 类型是 defaultdictdict(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.getkey in d 不触发),触发后会「真的插入」这个键。最隐蔽的坑读一个不存在的键(d[k])也会自动创建它(污染字典、len 变大),所以判断存在性用 key in d、读取用 d.get(k, default)、别用 d[k]vs setdefaultsetdefault(k, default) 每次调用都构造 default(即使键已存在),defaultdict 的工厂只在缺键时才调用(更省)。注意工厂必须「无参可调用」(defaultdict(list) 对、defaultdict(list()) 错——传的是空列表)。一句话「defaultdict(工厂)缺键时自动生成默认值并插入(分组用 list、计数用 int、去重用 set);靠重写__missing__(仅 d[key]触发、会插入键);读缺键也会插入是坑(判断用 in、读取用 get);比 setdefault 省(只缺键才调工厂);工厂要无参可调用别加括号」。