编程语言研究书库

默认字典 — 查不到的键,自动给默认值

默认字典 — 查不到的键,自动给默认值

这一章讲一件事: 用字典计数或分组时,最大的麻烦不是字典本身, 而是「这个键还不在字典里」这句没完没了的检查。 defaultdict(默认字典)把这句检查收进字典内部——查不到的键不报错, 自动给你一个事先约定的默认值。数单词、分小组、记访客, 三种最常见的分组计数活,每种都能少写一半的防守代码。

1. 这一章讲什么、在全书链条里的位置

上一章的 namedtuple 给元组加了名字;这一章的 defaultdict 给字典加了 「默认值」。它们都住在标准库的 collections 模块里,思路也一样: 不改原结构的本事,只改掉一个让你反复写样板代码的痛点1。

痛点具体是什么?看一个最普通的任务:数一串单词里每个词出现几次。 你手里的工具是普通字典,而普通字典的脾气是——键不存在,当场报错。

2. 顶层全景:同一个计数任务,三种写法

  任务:数 'hello world python world' 里每个词出现几次(答案是 hello 1、world 2、python 1)
      │
      ├─ 普通字典:每加一个词前,先问「这个词在不在?」不在就先建键 ── 3 行防守代码
      ├─ defaultdict(int):直接 word_count[word] += 1 ── 查不到?先放个 0 再 += 1
      └─ (下一章的 Counter:连循环都省掉,一行数完)

  图说:一行比一行少写代码,这是 collections 三件套的共同方向——把样板代码收进工具里。

主线例子用同一句话走全程:'hello world python world'——4 个词、3 种词, 正确答案:hello 1 次、world 2 次、python 1 次2。

3. 核心原理:三个机制,各走一步主线例子

3.1 先看清痛点:普通字典的两次报错

普通字典有两处会咬人,书里各给了一个演示。

第一处:取值。 my_dict = {'a': 1, 'b': 2},取 my_dict['a'] 没问题; 取 my_dict['c']——当场抛出一个 KeyError: 'c'(键错误:找不到 'c' 这个键)3。

第二处更隐蔽:累加。 想给键 'd' 的计数加 1,顺手写下 my_dict['d'] += 1——这行其实暗藏两步:先取 my_dict['d'] 的旧值,再加 1。 'd' 根本不在字典里,于是第一步就炸了,还是 KeyError4。 也就是说,+= 这种写法对不存在的键永远是错的,不管你多想当然。

所以老实的写法是先查后办,书里给了模板:循环里先 if key in my_dict, 在就加 1,不在就先 my_dict[key] = 15。 能跑,但每个计数任务都要重抄一遍这段防守——这就是样板代码。

3.2 机制:把「默认值工厂」交给字典

defaultdict 的解决办法一句话:造字典时告诉它「缺键时该放个什么」, 之后它替你放。 造法:

from collections import defaultdict
word_count = defaultdict(int)

括号里的 int 不是随便写的——它是「工厂」:一个被调用时就能造出默认值的东西。 int() 被调用时造出 0,所以 defaultdict(int) 的意思是: 查不到键时,先调一下 int() 造个 0 放进去,再继续你的操作。

于是一行 += 就合法了:

for word in 'hello world python world'.split():
    word_count[word] += 1

走一遍主线例子,每一步旁边是具体状态:

  取 'hello' → 不在 → 放 0 → 0+1=1    word_count 现在是 {'hello': 1}
  取 'world' → 不在 → 放 0 → 0+1=1    {'hello': 1, 'world': 1}
  取 'python' → 不在 → 放 0 → 0+1=1   {'hello': 1, 'world': 1, 'python': 1}
  取 'world' → 在!→ 1+1=2             {'hello': 1, 'world': 2, 'python': 1}

运行结果正是 {'hello': 1, 'world': 2, 'python': 1}2。 注意那个细节:查 'world' 第二次时键已存在,defaultdict 完全不动默认值, 照旧加 1——它只在缺键的那一下出手。

再回答一个自然的疑问:为什么是 int 而不是 0? 因为默认值要按需求现造而不是提前备好一份——工厂存的是「造 0 的方法」, 每次缺键现场调用一次。这解释了三个事:默认值总是崭新的(两个键各拿各的 0, 互不干扰);工厂可以是有参数的复杂构造(比如 list 造 []、set 造空集); 以及下一章的 Counter,本质上是一台「默认值为 0、还自带数数动作」的特化机器。

为什么有效: 那 3 行防守代码(在不在?不在就建)被你移交给了字典。 你写的代码只剩「业务动作」本身(+= 1),出错的机会跟着防守代码一起消失了。

3.3 三种默认值,三种分组场景

int 只是默认值工厂的一种。原书接着演示了另外两种,对应另外两类高频任务:

默认值工厂缺键时放进什么适合干什么本章场景
int0计数、累加数单词;黑客松里累计每个队的奖金
list[](空列表)按键分组收集把学生按课程分组、按城市分组
setset()(空集合)按键分组且组内去重记录每个网页的独立访客、每个部门的技能

分组走一遍(list 版)。 任务:把学生按课程归档。三条记录: Alice 和 Bob 修 CS,Charlie 修 Stats。写法是:

students_by_course = defaultdict(list)
students_by_course['CS'].append('Alice')
students_by_course['CS'].append('Bob')
students_by_course['Stats'].append('Charlie')

第一行 students_by_course['CS'].append('Alice') 其实也在暗藏两步: 先取 'CS' 这个键——不在,放进一个空列表 [];再对这个列表 append。 结果:{'CS': ['Alice', 'Bob'], 'Stats': ['Charlie']}6。 用普通字典写,三条 append 前面各得垫一句「键在不在」的判断。

组内去重走一遍(set 版)。 任务:记录每个网页的独立访客。 User1 访问了 homepage 两次——集合的本性会让第二次添加自动落空。 结果:{'homepage': {'User2', 'User1'}, 'contact': {'User1'}}—— homepage 的访客集合里,User1 只有一份7。

3.4 黑客松案例:两种默认值在同一个程序里搭班

章末案例把本章三种默认值里的两种放进同一个程序,管理一场黑客松: 三支队伍(Team A/B/C),各队两名队员,各队一笔奖金8。

teams = defaultdict(list)                    # 队员分组:缺键放空列表
teams['Team A'].extend(['Alice', 'Bob'])     # extend 一次塞进整个名单
teams['Team B'].extend(['Charlie', 'Dave'])
teams['Team C'].extend(['Eve', 'Frank'])

prizes = defaultdict(int)                    # 奖金累计:缺键放 0
prizes['Team A'] += 500
prizes['Team B'] += 300
prizes['Team C'] += 1000

走一遍,每一步旁边是具体状态:

  teams['Team A'].extend([...])  → 'Team A' 不在 → 放 [] → 塞入两人
                                   teams = {'Team A': ['Alice', 'Bob']}
  prizes['Team A'] += 500        → 'Team A' 不在 → 放 0 → 0+500=500
                                   prizes = {'Team A': 500}

最终打印:Team A: Alice, Bob / Team B: Charlie, Dave / Team C: Eve, Frank; 奖金 Team A $500、Team B $300、Team C $10008。 一个程序里两个 defaultdict 各管一件事——分组归分组,累计归累计—— 这就是「一个工具多处用」的示范,也是本章三种默认值的真实分工方式。

4. 作者的判断与证据

作者明说的: defaultdict 与 dict 的区别只有一行——「dict raises an error when a missing key is accessed, while defaultdict returns a default value and avoids the need for manual checking」9。

证据: 本章证据全是可运行的对照演示:同一段计数,先普通字典(炸), 再 defaultdict(通);同一段分组,先想想要写多少防守,再看实际三行。 章末案例(黑客松)把两种默认值放在同一个程序里用(3.4 节)8。

我们的解读(不是书里的): 三件套有一条共同的演进线,本章站在中间: 普通字典 → defaultdict(省去防守代码)→ Counter(下一章,连循环都替你写好)。 每往右一步,你写的代码就更接近「问题本身」而不是「怎么绕过工具的脾气」。

判断(我们的,不是书里的): 计数和分组是数据处理的两大地基动作, defaultdict 让这两动作的入门写法直接等于「成熟写法」——新手省掉的防守代码, 正是老手会漏写的那类。要留意的是它「访问即创建」的暗副作用: 纯探键在不在也会留下键,这在调试时会让人误以为程序多跑了一步。 如果错,会错在: 如果团队约定「字典必须是纯查询不动结构」,defaultdict 的隐式建键会违反直觉,这时先查后用反而更安全——取舍取决于代码由谁维护。

5. 边界与局限

默认值只对「访问缺失键」生效,不是万能纠错。 你把 defaultdict(list) 当计数器用(+= 1),加出来的还是 TypeError——放的是 [] 不是 0。 选哪种工厂,得和你打算对值做的操作对上号(书里 Table 17.1 把这四种配对 印成了对照表)10。

「查一下就建一个键」有时是惊喜有时是惊吓。 defaultdict 的访问即创建, 意味着你单纯想探一下「'c' 在不在」,它也把 'c' 真建出来了。 看这个现场:对一个空的 d = defaultdict(int) 问一句 d['x'], 得到的返回值是 0——可副作用是字典里从此有了 'x' 这个键; 再 print(d),出来的是 defaultdict(<class 'int'>, {'x': 0}), 那行尖括号里的工厂说明也是 defaultdict 的专属长相,普通字典打印时从不带它。 调试时若看到字典里冒出没见过的键,先怀疑是这类「探查留下的脚印」10。

重复键的累加语义要自己守。 黑客松练习里,order_count['Bob'] += 2 是「Bob 一次下了 2 单」的意思,结果 Bob 总共 2 单;若数据里 Bob 分两行各 1 单, 则 += 两次也是 2。defaultdict 只管「缺键放默认」,「一行是几单」永远是 你的数据说了算。

6. 可带走的

  1. 普通字典查不存在的键 → KeyError;+= 对不存在的键必炸——先查后办是老实写法;
  2. defaultdict(工厂):缺键时自动调工厂放一个默认值,之后照常运算;
  3. 计数用 defaultdict(int)(缺键放 0),分组用 defaultdict(list)(放 []), 分组且组内去重用 defaultdict(set)(放空集);
  4. 主线例子一行版:for word in text.split(): word_count[word] += 1;
  5. 它只替你省「键在不在」的检查,不替你选工厂——工厂选错照样报错;
  6. 访问即创建:只想探键在不在,也会留下这个键;
  7. 学到这里,数数这件事还有最后一级台阶:下一章的 Counter 把整段循环压成一行——三件套就此收齐。

本章的练习全是「先想想要写多少防守,再看实际三行」的对照式设计; 做顺之后,再看到「统计」「分组」两个字,你的手会先伸向 collections。

7. 原文地图

主题原书章原文位置
本章定位(省去手动检查)SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:153(搜「eliminating the need」)
普通字典取缺失键的 KeyErrorSECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:173(搜「KeyError: 'c'」)
+= 对缺失键的 KeyErrorSECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:196(搜「my_dict['d'] += 1」)
先查后办的样板代码SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:205(搜「key in my_dict」)
defaultdict 机制与 int 演示SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:231(搜「defaultdict(int)」) · :236(搜「default value」)
dict 与 defaultdict 的一行区别SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:244(搜「Key difference」)
数单词(主线例子)SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:261(搜「hello world python world」) · :270(搜「'world': 2」)
数订单练习SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:285(搜「order_count」)
list 分组:按课程、按城市SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:313(搜「students_by_course」) · :338(搜「people_by_city」)
set 去重分组:独立访客、部门技能SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:366(搜「unique_visitors」) · :389(搜「department_skills」)
黑客松案例(双 defaultdict)SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:414(搜「HACKATHON」) · :438(搜「teams」) · :444(搜「prizes」)
四种默认值对照表(Table 17.1)SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:469(搜「summarize the default dictionary」)
官方文档链接SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:507(搜「EXPLORE MORE」)
按城市分组练习SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:341(搜「New York」)

Footnotes

  1. 出处:「SECTION IV: Data Collections」第 149–159 段(text/25-fm-section-iv-data-collections.txt:153,搜「eliminating the need」)。原文章节引言:「automatically assigning a default value to missing keys, eliminating the need for manual checks or initialization」。 ↩

  2. 出处:「SECTION IV: Data Collections」第 250–270 段(text/25-fm-section-iv-data-collections.txt:261,搜「hello world python world」;:264,搜「text.split」;:270,搜「'world': 2」)。数单词的运行结果 {'hello': 1, 'world': 2, 'python': 1} 见原文第 270 段;逐步状态是我们的走查演绎,每步的数与原书结果一致。 ↩ ↩2

  3. 出处:「SECTION IV: Data Collections」第 162–180 段(text/25-fm-section-iv-data-collections.txt:173,搜「KeyError: 'c'」)。print(my_dict['c']) 抛 KeyError 的演示。 ↩

  4. 出处:「SECTION IV: Data Collections」第 182–199 段(text/25-fm-section-iv-data-collections.txt:196,搜「my_dict['d'] += 1」)。my_dict['d'] += 1 抛 KeyError: 'd'——+= 先取值后相加,取值的这步已炸。这个「暗藏两步」的解释是我们补的机制说明,原文只演示了报错。 ↩

  5. 出处:「SECTION IV: Data Collections」第 201–221 段(text/25-fm-section-iv-data-collections.txt:205,搜「key in my_dict」;:218,搜「my_dict[key] = 1」)。先查后办模板与运行结果 {'a': 1, 'b': 3, 'd': 1}(第 221 段)。 ↩

  6. 出处:「SECTION IV: Data Collections」第 304–323 段(text/25-fm-section-iv-data-collections.txt:313,搜「students_by_course」;:323,搜「'Stats'」)。按课程分组的结果 {'CS': ['Alice', 'Bob'], 'Stats': ['Charlie']}。 ↩

  7. 出处:「SECTION IV: Data Collections」第 357–376 段(text/25-fm-section-iv-data-collections.txt:366,搜「unique_visitors」;:372,搜「won't be added again」;:376,搜「User2」)。User1 重复访问 homepage 只留一份的运行结果。 ↩

  8. 出处:「SECTION IV: Data Collections」第 414–467 段(text/25-fm-section-iv-data-collections.txt:438,搜「teams」;:444,搜「prizes」;:467,搜「Team C: $1000」)。黑客松案例:defaultdict(list) 收队员、defaultdict(int) 累计奖金,运行结果见第 459–467 段。 ↩ ↩2 ↩3

  9. 出处:「SECTION IV: Data Collections」第 244–246 段(text/25-fm-section-iv-data-collections.txt:244,搜「Key difference」)。原文:「dict raises an error when a missing key is accessed, while defaultdict returns a default value and avoids the need for manual checking」。 ↩

  10. 出处:「SECTION IV: Data Collections」第 469–486 段(text/25-fm-section-iv-data-collections.txt:469,搜「summarize the default dictionary」)。Table 17.1 给出 list/int/str/set 四种默认值的对照(创建写法、默认值、典型用法)。 ↩ ↩2

On this page