这是一份关于默认字典那个东西的深度指南, 它会告诉你所有的细节。
在编程的日常实践这一件事情上面, 字典这种东西也就是通常说的dict, 是我们处理有关联关系的数据时候最频繁使用到的一个工具。然而标准字典有一个特性, 这个特性让开发者既喜爱又抱怨, 其具体表现是当你尝试去访问一个本来就不存在的键的时候, 它会毫不犹豫地抛出一些异常来作为回应。
为了躲避这种异常的麻烦发生, 我们经常需要不得不写下大量的代码来判断是否包含某个键, 比如那种if key in dict的写法, 或者是去使用一些比较长并且显得累赘的方法调用方式, 例如那些dict开头的函数调用。
为了解决这个让人头疼的实际问题, 在那个特定的软件模块里面, 给大家提供了一个用起来特别顺手、能大大提高效率的好帮手, 不过原句里没写清楚到底是叫啥名字。这个东西其实就是咱们平时见得很多的标准字典的一种特殊变种类型。
它俩之间最大的不同点, 也基本上就是全部的不同点, 只有一个: 它在底层某个比较关键的运行方法上做了重新编写和调整, 这样一来呢, 当我们试图去查找某个在字典里原本根本不存在的密钥也就是找不到具体对应的值的时候, 系统就不会直接报错或者返回空值, 而是会自动地按照我们提前设定好的一个专门用来生成值的函数, 也就是那个所谓的工厂函数, 来现场产生并赋予一个默认的数值给我们。
本教程会带大家深入到它的内心世界, 内容会从最基本的语法开始, 一直讲到比较复杂的数据聚合的场景, 对整个这个能够让代码的行数瞬间就变少一半的神奇容器进行全面解析。
text
+-----------------------------------------------------------+
| |
| PYTHON DEFAULTDICT MECHANISM |
| |
| Access Key "X" |
| | |
| v |
| Does "X" exist?
----(Yes)----> Return Value |
| | |
| (No) |
| | |
| v |
| Call default_factory() ----> Create New Default Value |
| | & Assign to "X" |
| v |
| Return New Value |
| |
+-----------------------------------------------------------+
默认字典的核心哲学是, 去优雅地处理那些在字典里压根就不存在的缺失情况。
这个事物的核心哲学存在下面这种意思, 意思就是预测到数据里面可能会有缺少的情况, 并且要给它给出一个开始的数值。我们在做数据处理的工作的时候, 非常普遍会用到把数据弄成分组或者是去数清楚个数这样的情况。
假如说你用的是那种很标准的字典数据结构的话, 那你就一定得先去仔细检查一下某一个分类是不是已经存在了。如果检查结果是它并不存在, 那就必须赶紧把这个东西初始化成一个空的列表或者初始值零, 然后再去做接下来的操作动作。这一套复杂的逻辑流程现在已经变成了被自动去执行的状态了。
- 为什么说它要比那个()好?
标准字典中的 ( key, ) 也可以实现类似于这样的功能, 但是这种做法存在两个问题。
在每次进行调用的时候, 无论所指定的<强>键到底存不存在, 那个默认的值的对象都是一定会被创建出来一次的。
关于代码冗余方面, 你现在遇到的情况是, 在每次进行访问操作的时候, 都非得把默认值重新写一遍不可。 但是呢, 如果只是单纯地进行初始化声明的话, 那么只需要写上一次就够了。
- 咱们来说一下这个底层的原理, 具体是采用什么样的方法呢。
之所以可以实现自动初始化这样的操作, 其实是因为它用到了字典协议里面的 (key) 这个特殊的方法。
如果在找某个键的时候怎么也找不到结果的话, 它就会去看一看那个属性到底是个什么状态, 只要发现该属性的值不是 None, 那么就会把之前定义的那个工厂函数给调用起来, 然后把工厂函数跑出来的结果存到字典里面去。
的基础入门
你在使用它之前, 必须先从 模块里面把它导入到当前的代码环境中来。
- 用list来当默认的那个值, 去实现一对多的那种映射关系。
这是使用场景当中非常经典的一种情况。当你需要把多个不同的值, 对应到同一个键上面的时候, 比如说像将学生按照班级进行分组这样的操作一样, 这种方式能够大大地简化程序逻辑的实现过程。
python
from collections import defaultdict
# 声明工厂函数为 list
groups = defaultdict(list)
# 无需判断 "Python" 键是否存在,直接 append
groups['Python'].append('Shyam')
groups['Python'].append('Alice')
groups['Java'].append('Bob')
print(groups)
# 结果: {'Python': ['Shyam', 'Alice'], 'Java': ['Bob']}
- 采取int这种类型, 把它设置成默认的状态, 这样子的话就实现了那个计数的功能。
当我们把int这个工厂函数给使用的过程中, 它的默认值就是那个数字0的。这种状况呢, 对于咱们需要进行的那个词频统计的操作来说, 是非常适合的一个特点。
python
text = "apple banana apple orange banana apple"
counter = defaultdict(int)
for word in text.split():
# 第一次遇到 word 时,会自动初始化为 0,然后 +1
counter[word] += 1
print(counter['apple']) # 输出 3
深入地来进行探索, 去搞清楚到底是怎么一回事, 关于工厂函数的各种形态。
它并不仅限于那些内置的数据类型, 它可以变成任何能够被调用的对象, 也就是那些可以在需要的时候进行调用操作的实体。
- 通过使用的这种方式, 我们可以对自定义的默认值进行设定。

你要是觉得那个默认的值, 他不应该是0也好也不应该是一个空的状态, 反而想让它变成某个确定的数字, 或者某段特定的文字的话, 你可以通过使用匿名函数这种方式来实现。
python
# 默认分数为 60 分
grades = defaultdict(lambda: 60)
grades['Shyam'] = 95
print(grades['Shyam']) # 95
print(grades['Unknown']) # 60
- 所谓的嵌套, 其实际含义就是通过这种方式去实现那些具有多层次结构特征的数据体系。
你把那个所谓的本身当作一个工厂函数来用, 这样就可以弄出一个类似多维字典这样的结构。
python
# 创建一个可以无限嵌套的字典结构
def tree():
return defaultdict(tree)
users = tree()
users['shyam']['settings']['theme'] = 'dark'
# 这种写法在处理复杂的 JSON 样数据时非常丝滑
行为差异与潜在陷阱
虽然它确实是比较方便的, 但是, 它的这种做法, 跟那种普通的、咱们最常见的字典相比, 在一些非常小的细节上面, 是有那么一点不同的, 要是你要是没有太注意这个问题的话, 那很有可能会产生一些逻辑上面的错误的。
- 只要访问这个内容, 它就会被直接创建出来。
这是最容易被大家所忽略的一个陷阱所在, 其实你只要仅仅去做一次访问这个动作, 比方说把它打印出来看看, 或者是拿去当作一个判断条件去使用, 那么就算是一个本来不存在的键, 它都会自然而然地被添加到字典里去。
python
d = defaultdict(int)
print(d['nothing']) # 输出 0
print(len(d)) # 输出 1,键 'nothing' 已经被创建了!
当我们进行逻辑判断的时候, 如果你只是想要检查一个键是否存在, 并且不希望触发那个东西被创建的动作的话, 那么请你使用 if key in d 这种方式去操作。
- 工厂函数的调用时机
这个机制只有在你采用特定的访问方式的时候才会被激活。具体来说, 当缺失的键通过"d< strong>语法被访问的时候, 会触发相应的行为。
但如果你选择使用d点get方括号key圆括号这种形式来进行操作的话, 它的运行表现就会恢复到普通字典那种状态。也就是说, 在这种情况下, 如果所要查找的键并不存在, 那么系统会直接返回None这个值。与此同时, 新条目也就不会在此时被创建出来。
关于性能分析与工业实战的, 这一点是集中在性能表现上的。
在大量的查找和初始化循环中, 的速度通常比手动检查快约 10% 到 20%。这是因为它减少了 解释层面的字节码执行量,将初始化逻辑下放到了 C 语言实现的底层。
- 在实际操作的过程之中, 对日志数据展开详细的分析与解析。
我们现在假定手里拿到的情况是这样子的, 就是一份 Web 服务器里面的日志数据, 然后我们的目的就是要去把这些数据给统计一下, 具体来说, 是要去算出每一个单独的 IP 地址在各个不同的时间段里面究竟访问了多少次。
python
from collections import defaultdict
log_data = [
('192.168.1.1', '10:00'),
('192.168.1.2', '10:00'),
('192.168.1.1', '11:00'),
]
# 结构:{ IP: { Time: Count } }
stats = defaultdict(lambda: defaultdict(int))
for ip, time in log_data:
stats[ip][time] += 1
# 访问数据时也非常直观
print(stats['192.168.1.1']['10:00']) # 1
总结一下, 这里所涉及到的关于<的开发准则的那些内容。
这是代码的一个非常出色的代表性例子, 它的做法是通过将所谓的初始化这一环节和具体的业务逻辑这部分内容进行分离开来的方式, 从而使得我们核心的那些代码变得更加纯粹干净一些。
对于分组这个操作, 首选的方案是任何涉及到列表或者集合进行聚合的场景, 这种情况下大家应该优先考虑使用。在计数的方面, 它就像是一个利器, 具体来说, 是在并不需要那些点号开头的高级特性的时候, 整数类型是最轻量级的计数办法。
另外需要警惕的是空访问的问题, 也就是说, 在对字典实施只读遍历或者是条件判断这个动作的时候, 需要注意不要意外地去创建出了大量的默认条目。你需要明确的就是工厂这个东西。你要确保传入的是函数名字, 比如说是 list。
而不是函数调用, 比如说是 list()。关于类型转换这块内容, 如果你需要把处理完毕之后的数据传递给那些不熟悉这个类型的第三方库, 那么你可以随时使用 dict(d) 这种方式把它们转回普通的字典状态里面去。
等你把东西都弄明白了, 你就不会再像个在那些复杂事物面前紧张发抖的修补匠了, 你现在可以算是一个能把大规模关联数据轻松驾驭住的架构师。
在接下来的这一章里, 我们会去讨论一下 deque 这个概念, 同时我们也来看看当它处理那些要求很苛刻的双端操作的时候, 队列是怎么把它那种像爆炸一样厉害的潜能给发挥出来的呢。
text
+-----------------------------------------------------------+
| DEFAULTDICT 操作清单 |
+-----------------------------------------------------------+
| 1. 初始化时传入的是函数对象,如 int, list, set 或 lambda。
|
| 2. 只有 d[key] 访问会触发默认值创建,d.get() 不会。 |
| 3. 若需实现多级嵌套,可使用 lambda: defaultdict(factory)。
|
| 4. 在不需要自动创建的功能时,应及时转回普通 dict 以节省内存。 |
| 5. 它是 dict 的子类,所有 dict 的方法(items, keys)均适用。
|
+-----------------------------------------------------------+