使用 Python defaultdict 类型处理丢失的键
目录
使用 Python字典时可能会遇到的一个常见问题是尝试访问或修改字典中不存在的键。这将引发 aKeyError
并中断您的代码执行。为了处理这些情况,标准库提供了 Pythondefaultdict
类型,这是一个类似字典的类,在collections
.
Pythondefaultdict
类型的行为几乎与常规 Python 字典完全相同,但是如果您尝试访问或修改缺少的键,defaultdict
则将自动创建该键并为其生成默认值。这defaultdict
为处理字典中丢失的键提供了一个有价值的选择。
在本教程中,您将学习:
- 如何使用 Python
defaultdict
类型处理字典中缺失的键 - 何时以及为何使用 Python
defaultdict
而不是常规dict
- 如何使用 a
defaultdict
进行分组、计数和累加操作
掌握这些知识后,您将能够更好地defaultdict
在日常编程挑战中有效地使用 Python类型。
为了充分利用本教程,您应该事先了解 Python词典是什么以及如何使用它们。如果您需要焕然一新,请查看以下资源:
- Python 中的字典(教程)
- Python 中的字典(课程)
- 如何在 Python 中遍历字典
处理字典中的缺失键
使用 Python 字典时可能面临的一个常见问题是如何处理丢失的键。如果您的代码很大程度上基于字典,或者您一直在动态创建字典,那么您很快就会注意到处理频繁的KeyError
异常可能会非常烦人,并且会给您的代码增加额外的复杂性。使用 Python 字典,您至少有四种可用的方法来处理丢失的键:
- 利用
.setdefault()
- 利用
.get()
- 使用
key in dict
成语 - 使用 a
try
和except
块
在Python文档说明.setdefault()
和.get()
如下:
setdefault(key[, default])
如果
key
在字典中,则返回其值。如果不是,则插入key
值为default
并返回default
。default
默认为None
.
get(key[, default])
返回
key
ifkey
在字典中的值, elsedefault
。如果default
未给出,则默认为None
,因此此方法永远不会引发KeyError
。(来源)
下面是一个如何.setdefault()
处理字典中缺失键的示例:
>>> a_dict = {}
>>> a_dict['missing_key']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
a_dict['missing_key']
KeyError: 'missing_key'
>>> a_dict.setdefault('missing_key', 'default value')
'default value'
>>> a_dict['missing_key']
'default value'
>>> a_dict.setdefault('missing_key', 'another default value')
'default value'
>>> a_dict
{'missing_key': 'default value'}
在上面的代码中,您用于.setdefault()
为 生成默认值missing_key
。请注意,您的字典 ,a_dict
现在有一个名为missing_key
的新键,其值为'default value'
。在您调用 之前,此密钥不存在.setdefault()
。最后,如果您调用.setdefault()
现有的键,则调用不会对字典产生任何影响。您的密钥将保存原始值而不是新的默认值。
注意:在上面的代码示例中,您会收到一个异常,并且 Python 会向您显示一条回溯消息,该消息告诉您您正在尝试访问a_dict
. 如果您想更深入地了解如何破译和理解 Python 回溯,请查看了解 Python 回溯和充分利用 Python 回溯。
另一方面,如果您使用.get()
,那么您可以编写如下代码:
>>> a_dict = {}
>>> a_dict.get('missing_key', 'default value')
'default value'
>>> a_dict
{}
在这里,您使用.get()
为 生成默认值missing_key
,但这次,您的字典保持为空。这是因为.get()
返回默认值,但此值未添加到基础字典中。例如,如果您有一本名为 的字典D
,那么您可以假设它的.get()
工作原理如下:
D.get(key, default) -> D[key] if key in D, else default
使用此伪代码,您可以了解.get()
内部是如何工作的。如果键存在,则.get()
返回映射到该键的值。否则,返回默认值。您的代码永远不会创建或分配值key
。在本例中,default
默认为None
。
您还可以使用条件语句来处理字典中缺失的键。看看下面的例子,它使用了这个key in dict
习语:
>>> a_dict = {}
>>> if 'key' in a_dict:
... # Do something with 'key'...
... a_dict['key']
... else:
... a_dict['key'] = 'default value'
...
>>> a_dict
{'key': 'default value'}
在此代码,您使用的if
语句与一起in
操作,以检查是否key
存在a_dict
。如果是这样,那么您可以使用key
或使用其值执行任何操作。否则,您将创建新密钥key
,并为其分配一个'default value'
。请注意,上述代码的工作原理类似于.setdefault()
但需要四行代码,而.setdefault()
只需要一行(除了更具可读性之外)。
您还可以KeyError
使用 atry
和except
块来处理异常。考虑下面的一段代码:
>>> a_dict = {}
>>> try:
... # Do something with 'key'...
... a_dict['key']
... except KeyError:
... a_dict['key'] = 'default value'
...
>>> a_dict
{'key': 'default value'}
上面示例中的try
andexcept
块会在KeyError
您尝试访问丢失的密钥时捕获。在except
子句中,您创建key
并为其分配一个'default value'
。
注意:如果缺少的键在您的代码中并不常见,那么您可能更喜欢使用 atry
和except
块(EAFP 编码风格)来捕获KeyError
异常。这是因为代码不会检查每个键是否存在,并且只处理少数异常(如果有)。
另一方面,如果缺少键在您的代码中很常见,那么条件语句(LBYL 编码风格)可能是更好的选择,因为检查键的成本比处理频繁异常的成本更低。
到目前为止,您已经学会了如何使用dict
Python 提供的工具处理丢失的键。但是,您在此处看到的示例非常冗长且难以阅读。它们可能不像您想要的那么简单。这就是Python 标准库提供更优雅、Pythonic和高效的解决方案的原因。该解决方案是collections.defaultdict
,这就是您从现在开始要介绍的内容。
理解 Pythondefaultdict
类型
Python 标准库提供了collections
,它是一个实现专用容器类型的模块。其中之一是 Pythondefaultdict
类型,它是dict
专为帮助您解决丢失键而设计的替代方法。defaultdict
是一种继承自的 Python 类型dict
:
>>> from collections import defaultdict
>>> issubclass(defaultdict, dict)
True
上面的代码显示,Python的defaultdict
类型是子类的dict
。这意味着defaultdict
继承了 的大部分行为dict
。所以,你可以说这defaultdict
很像一本普通的字典。
defaultdict
和之间的主要区别在于dict
,当您尝试访问或修改key
字典中不存在的 时,value
会自动为该提供默认值key
。为了提供这个功能,Pythondefaultdict
类型做了两件事:
- 它覆盖
.__missing__()
. - 它添加了
.default_factory
一个需要在实例化时提供的可写实例变量。
实例变量.default_factory
将保存传入的第一个参数defaultdict.__init__()
。此参数可以采用有效的 Python 可调用或None
. 如果提供了可调用对象,则defaultdict
每当您尝试访问或修改与缺失键关联的值时,它都会自动被调用。
注意:类初始值设定项的所有剩余参数都被视为传递给常规的初始值设定项dict
,包括关键字参数。
看看如何创建和正确初始化 a defaultdict
:
>>> # Correct instantiation
>>> def_dict = defaultdict(list) # Pass list to .default_factory
>>> def_dict['one'] = 1 # Add a key-value pair
>>> def_dict['missing'] # Access a missing key returns an empty list
[]
>>> def_dict['another_missing'].append(4) # Modify a missing key
>>> def_dict
defaultdict(<class 'list'>, {'one': 1, 'missing': [], 'another_missing': [4]})
在这里,您在创建字典时传递list
到.default_factory
。然后,您def_dict
就像使用普通字典一样使用。请注意,当您尝试访问或修改映射到不存在的键的值时,字典会为其分配调用list()
.
请记住,您必须将有效的 Python 可调用对象传递给.default_factory
,因此请记住不要在初始化时使用括号调用它。当您开始使用 Pythondefaultdict
类型时,这可能是一个常见问题。看看下面的代码:
>>> # Wrong instantiation
>>> def_dict = defaultdict(list())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
def_dict = defaultdict(list())
TypeError: first argument must be callable or None
在这里,您尝试defaultdict
通过传递list()
给.default_factory
. 对 的调用list()
引发 a TypeError
,它告诉您第一个参数必须是可调用的 or None
。
通过对 Pythondefaultdict
类型的介绍,您可以通过实际示例开始编码。接下来的几节将带您了解一些常见的用例,您可以在这些用例中依靠 adefaultdict
提供优雅、高效和 Pythonic 的解决方案。
使用 Pythondefaultdict
类型
有时候,你会使用内置集合一个可变的(一个list
,dict
或set
)在你的Python字典值。在这些情况下,您需要在第一次使用前初始化密钥,否则您会得到一个KeyError
. 您可以手动执行此过程,也可以使用 Python 自动执行此过程defaultdict
。在本节中,您将学习如何使用 Pythondefaultdict
类型来解决一些常见的编程问题:
- 将集合中的项目分组
- 计算集合中的项目
- 累积集合中的值
您将介绍一些使用list
、set
、int
和float
以用户友好且高效的方式执行分组、计数和累加操作的示例。
分组项目
Pythondefaultdict
类型的典型用途是设置.default_factory
为list
然后构建一个将键映射到值列表的字典。有了这个defaultdict
,如果您尝试访问任何丢失的键,则字典将运行以下步骤:
- 调用
list()
创建一个新的空list
- 插入空的
list
使用缺少键进入词典key
- 返回对那个的引用
list
这允许您编写如下代码:
>>> from collections import defaultdict
>>> dd = defaultdict(list)
>>> dd['key'].append(1)
>>> dd
defaultdict(<class 'list'>, {'key': [1]})
>>> dd['key'].append(2)
>>> dd
defaultdict(<class 'list'>, {'key': [1, 2]})
>>> dd['key'].append(3)
>>> dd
defaultdict(<class 'list'>, {'key': [1, 2, 3]})
在这里,您创建一个defaultdict
名为的 Pythondd
并传递list
给.default_factory
. 请注意,即使key
没有定义,你可以追加值,它没有得到一个KeyError
。那是因为dd
自动调用.default_factory
为缺少的key
.
您可以使用defaultdict
withlist
对序列或集合中的项目进行分组。假设您已从公司的数据库中检索到以下数据:
部门 | 员工姓名 |
---|---|
Sales | John Doe |
Sales | Martin Smith |
Accounting | Jane Doe |
Marketing | Elizabeth Smith |
Marketing | Adam Doe |
… | … |
有了这些数据,您创建初始list
的tuple
像下列对象:
dep = [('Sales', 'John Doe'),
('Sales', 'Martin Smith'),
('Accounting', 'Jane Doe'),
('Marketing', 'Elizabeth Smith'),
('Marketing', 'Adam Doe')]
现在,您需要创建一个按部门对员工进行分组的字典。为此,您可以使用 adefaultdict
如下:
from collections import defaultdict
dep_dd = defaultdict(list)
for department, employee in dep:
dep_dd[department].append(employee)
在这里,您创建一个defaultdict
被调用的对象dep_dd
并使用for
循环来遍历您的dep
列表。该语句dep_dd[department].append(employee)
为部门创建键,将它们初始化为一个空列表,然后将员工附加到每个部门。运行此代码后,您dep_dd
将看起来像这样:
defaultdict(<class 'list'>, {'Sales': ['John Doe', 'Martin Smith'],
'Accounting' : ['Jane Doe'],
'Marketing': ['Elizabeth Smith', 'Adam Doe']})
在此示例中,您将使用defaultdict
with.default_factory
设置为按部门对员工进行分组list
。要使用常规字典执行此操作,您可以使用dict.setdefault()
以下方法:
dep_d = dict()
for department, employee in dep:
dep_d.setdefault(department, []).append(employee)
这段代码很简单,作为 Python 编码员,您在工作中经常会发现类似的代码。然而,该defaultdict
版本可以说更具可读性,而且对于大型数据集,它也可以更快、更高效。因此,如果您关心速度,那么您应该考虑使用 adefaultdict
而不是标准dict
.
对唯一项进行分组
继续使用上一节中的部门和员工数据。经过一些处理,您发现数据库中错误地复制了一些员工。您需要清理数据并从dep_dd
字典中删除重复的员工。为此,您可以使用 a set
as the.default_factory
并按如下方式重写您的代码:
dep = [('Sales', 'John Doe'),
('Sales', 'Martin Smith'),
('Accounting', 'Jane Doe'),
('Marketing', 'Elizabeth Smith'),
('Marketing', 'Elizabeth Smith'),
('Marketing', 'Adam Doe'),
('Marketing', 'Adam Doe'),
('Marketing', 'Adam Doe')]
dep_dd = defaultdict(set)
for department, employee in dep:
dep_dd[department].add(employee)
在本例中,您设置.default_factory
为set
。集合是唯一对象的集合,这意味着您不能创建set
具有重复项的集合。这是集合的一个非常有趣的特性,它保证您在最终字典中不会有重复的项目。
计数项目
如果您设置.default_factory
为int
,那么您defaultdict
将有助于计算序列或集合中的项目。当您int()
不带参数调用时,该函数返回0
,这是您用来初始化计数器的典型值。
继续以公司数据库为例,假设您要构建一个字典来计算每个部门的员工人数。在这种情况下,您可以编写如下代码:
>>> from collections import defaultdict
>>> dep = [('Sales', 'John Doe'),
... ('Sales', 'Martin Smith'),
... ('Accounting', 'Jane Doe'),
... ('Marketing', 'Elizabeth Smith'),
... ('Marketing', 'Adam Doe')]
>>> dd = defaultdict(int)
>>> for department, _ in dep:
... dd[department] += 1
>>> dd
defaultdict(<class 'int'>, {'Sales': 2, 'Accounting': 1, 'Marketing': 2})
在这里,您设置.default_factory
为int
。int()
不带参数调用时,返回值为0
。您可以使用此默认值开始计算在每个部门工作的员工数。要使此代码正常工作,您需要一个干净的数据集。不得有重复数据。否则,您需要过滤掉重复的员工。
另一个计算项目的例子是计算mississippi
一个单词中每个字母重复的次数。看看下面的代码:
>>> from collections import defaultdict
>>> s = 'mississippi'
>>> dd = defaultdict(int)
>>> for letter in s:
... dd[letter] += 1
...
>>> dd
defaultdict(<class 'int'>, {'m': 1, 'i': 4, 's': 4, 'p': 2})
在上面的代码中,您创建了一个defaultdict
with .default_factory
set to int
。这将任何给定键的默认值设置为0
。然后,您使用for
循环遍历字符串 s
并使用增强赋值操作1
在每次迭代中添加到计数器。dd
will的键是 中的字母mississippi
。
注意: Python 的增强赋值运算符是常见操作的便捷快捷方式。
看看下面的例子:
var += 1
相当于var = var + 1
var -= 1
相当于var = var - 1
var *= 1
相当于var = var * 1
这只是增强赋值运算符如何工作的一个示例。您可以查看官方文档以了解有关此功能的更多信息。
由于计数是编程中相对常见的任务,Python 类字典类collections.Counter
专门用于对序列中的项目进行计数。使用Counter
,您可以编写mississippi
如下示例:
>>> from collections import Counter
>>> counter = Counter('mississippi')
>>> counter
Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
在这种情况下,Counter
为您完成所有工作!你只需要传入一个序列,字典就会计算它的项目,将它们存储为键,将计数存储为值。请注意,此示例有效,因为 Python 字符串也是序列类型。
累积值
有时您需要计算序列或集合中值的总和。假设您有以下Excel 表格,其中包含有关 Python 网站销售的数据:
产品 | 七月 | 八月 | 九月 |
---|---|---|---|
Books | 1250.00 | 1300.00 | 1420.00 |
Tutorials | 560.00 | 630.00 | 750.00 |
Courses | 2500.00 | 2430.00 | 2750.00 |
接下来,您可以使用Python和得到如下处理数据list
的tuple
对象:
incomes = [('Books', 1250.00),
('Books', 1300.00),
('Books', 1420.00),
('Tutorials', 560.00),
('Tutorials', 630.00),
('Tutorials', 750.00),
('Courses', 2500.00),
('Courses', 2430.00),
('Courses', 2750.00),]
使用此数据,您希望计算每个产品的总收入。为此,您可以使用defaultdict
带有float
as的 Python ,.default_factory
然后编写如下代码:
1from collections import defaultdict
2
3dd = defaultdict(float)
4for product, income in incomes:
5 dd[product] += income
6
7for product, income in dd.items():
8 print(f'Total income for {product}: ${income:,.2f}')
下面是这段代码的作用:
- 在第 1 行中,您导入 Python
defaultdict
类型。 - 在第 3 行中,您创建了一个设置为的
defaultdict
对象。.default_factory
float
- 在第 4 行中,您定义了一个
for
循环来遍历 的项目incomes
。 - 在第 5 行中,您使用增广赋值操作 (
+=
) 来累积字典中每个产品的收入。
第二个循环遍历 的项目dd
并将收入打印到您的屏幕上。
注意:如果您想更深入地了解字典迭代,请查看如何在 Python 中迭代字典。
如果您将所有这些代码放入一个名为的文件中incomes.py
并从命令行运行它,那么您将获得以下输出:
$ python3 incomes.py
Total income for Books: $3,970.00
Total income for Tutorials: $1,940.00
Total income for Courses: $7,680.00
您现在拥有每个产品的收入摘要,因此您可以决定遵循哪种策略来增加网站的总收入。
深入了解 defaultdict
到目前为止,您已经defaultdict
通过编写一些实际示例学习了如何使用 Python类型。此时,您可以更深入地了解类型实现和其他工作细节。这就是您将在接下来的几节中介绍的内容。
defaultdict
对比 dict
为了更好地理解 Pythondefaultdict
类型,一个很好的练习是将它与其超类dict
. 如果您想知道特定于 Pythondefaultdict
类型的方法和属性,则可以运行以下代码行:
>>> set(dir(defaultdict)) - set(dir(dict))
{'__copy__', 'default_factory', '__missing__'}
在上面的代码,你可以使用dir()
以获取有效的属性列表dict
和defaultdict
。然后,您使用set
差异来获取只能在defaultdict
. 如您所见,这两个类之间的区别是。您有两种方法和一种实例属性。下表显示了方法和属性的用途:
方法或属性 | 描述 |
---|---|
.__copy__() |
提供支持 copy.copy() |
.default_factory |
保存调用的可调用对象.__missing__() 以自动为缺少的键提供默认值 |
.__missing__(key) |
.__getitem__() 找不到时调用key |
在上表中,你可以看到,做的方法和属性defaultdict
从一个普通的不同dict
。其余的方法在两个类中都是相同的。
注意:如果您defaultdict
使用有效的可调用对象初始化 a ,那么KeyError
当您尝试访问丢失的密钥时,您将不会得到 a 。任何不存在的键都会获得由 返回的值.default_factory
。
此外,您可能会注意到 adefaultdict
等于dict
具有相同项的 a:
>>> std_dict = dict(numbers=[1, 2, 3], letters=['a', 'b', 'c'])
>>> std_dict
{'numbers': [1, 2, 3], 'letters': ['a', 'b', 'c']}
>>> def_dict = defaultdict(list, numbers=[1, 2, 3], letters=['a', 'b', 'c'])
>>> def_dict
defaultdict(<class 'list'>, {'numbers': [1, 2, 3], 'letters': ['a', 'b', 'c']})
>>> std_dict == def_dict
True
在这里,您创建一个std_dict
包含一些任意项目的常规字典。然后,您defaultdict
使用相同的项目创建一个。如果您测试两个词典的内容是否相等,那么您会发现它们是相等的。
defaultdict.default_factory
Pythondefaultdict
类型的第一个参数必须是一个不带参数并返回一个值的可调用对象。此参数分配给实例属性,.default_factory
。为此,您可以使用任何可调用对象,包括函数、方法、类、类型对象或任何其他有效的可调用对象。默认值.default_factory
是None
。
如果您在defaultdict
不将值传递给 的情况下进行实例化.default_factory
,则字典的行为将与常规类似dict
,并且通常KeyError
会因缺少键查找或修改尝试而引发:
>>> from collections import defaultdict
>>> dd = defaultdict()
>>> dd['missing_key']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
dd['missing_key']
KeyError: 'missing_key'
在这里,您将defaultdict
不带参数地实例化 Python类型。在这种情况下,实例的行为类似于标准字典。因此,如果您尝试访问或修改丢失的密钥,那么您将获得通常的KeyError
. 从现在开始,您可以将其dd
用作普通的 Python 字典,除非您将新的可调用对象分配给 ,否则.default_factory
您将无法使用defaultdict
自动处理丢失的键的功能。
如果您传递None
给 的第一个参数defaultdict
,则该实例的行为与您在上述示例中看到的相同。那是因为.default_factory
默认为None
,所以两个初始化是等效的。另一方面,如果将有效的可调用对象传递给.default_factory
,则可以使用它以用户友好的方式处理丢失的键。这是您传递list
给的示例.default_factory
:
>>> dd = defaultdict(list, letters=['a', 'b', 'c'])
>>> dd.default_factory
<class 'list'>
>>> dd
defaultdict(<class 'list'>, {'letters': ['a', 'b', 'c']})
>>> dd['numbers']
[]
>>> dd
defaultdict(<class 'list'>, {'letters': ['a', 'b', 'c'], 'numbers': []})
>>> dd['numbers'].append(1)
>>> dd
defaultdict(<class 'list'>, {'letters': ['a', 'b', 'c'], 'numbers': [1]})
>>> dd['numbers'] += [2, 3]
>>> dd
defaultdict(<class 'list'>, {'letters': ['a', 'b', 'c'], 'numbers': [1, 2, 3]})
在此示例中,您创建了一个defaultdict
名为的 Python dd
,然后将list
其用作第一个参数。调用第二个参数letters
并保存一个字母列表。您会看到,它.default_factory
现在拥有一个list
对象,当您需要value
为任何丢失的键提供默认值时将调用该对象。
请注意,当您尝试访问时numbers
,会dd
测试是否numbers
在字典中。如果不是,则调用.default_factory()
. 由于.default_factory
持有一个list
对象,返回的value
是一个空列表([]
)。
现在dd['numbers']
已用空初始化list
,您可以使用.append()
将元素添加到list
. 您还可以使用增强赋值运算符 ( +=
) 来连接列表[1]
和[2, 3]
。这样,您可以以更 Pythonic 和更有效的方式处理丢失的键。
另一方面,如果您将不可调用的对象传递给Pythondefaultdict
类型的初始化程序,那么您将TypeError
在以下代码中得到类似的结果:
>>> defaultdict(0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
defaultdict(0)
TypeError: first argument must be callable or None
在这里,您传递0
到.default_factory
。由于0
不是可调用对象,您会得到一个TypeError
告诉您第一个参数必须是可调用的或None
. 否则,defaultdict
不起作用。
请记住,.default_factory
它仅从.__getitem__()
其他方法调用,而不是从其他方法调用。这意味着 if dd
is a defaultdict
and key
is a missing key, then dd[key]
will call .default_factory
to provide a default value
,但dd.get(key)
仍然返回None
而不是提供的值.default_factory
。那是因为.get()
没有调用.__getitem__()
来检索key
.
看看下面的代码:
>>> dd = defaultdict(list)
>>> # Calls dd.__getitem__('missing')
>>> dd['missing']
[]
>>> # Don't call dd.__getitem__('another_missing')
>>> print(dd.get('another_missing'))
None
>>> dd
defaultdict(<class 'list'>, {'missing': []})
在此代码片段中,您可以看到dd.get()
返回None
值而不是提供的默认值.default_factory
。那是因为.default_factory
只调用 from .__missing__()
,而不调用.get()
.
请注意,您还可以向 Python 中添加任意值defaultdict
。这意味着您不限于与由 生成的值具有相同类型的值.default_factory
。下面是一个例子:
>>> dd = defaultdict(list)
>>> dd
defaultdict(<class 'list'>, {})
>>> dd['string'] = 'some string'
>>> dd
defaultdict(<class 'list'>, {'string': 'some string'})
>>> dd['list']
[]
>>> dd
defaultdict(<class 'list'>, {'string': 'some string', 'list': []})
在这里,您创建 adefaultdict
并将list
对象传递给.default_factory
。这会将您的默认值设置为空列表。但是,您可以自由添加包含不同类型值的新键。key 就是这种情况string
,它保存一个str
对象而不是一个list
对象。
最后,您始终可以像处理任何实例属性一样更改或更新最初分配给的可调用对象.default_factory
:
>>> dd.default_factory = str
>>> dd['missing_key']
''
在上面的代码中,您.default_factory
从更改list
为str
。现在,每当您尝试访问丢失的密钥时,您的默认值将是一个空字符串 ( ''
)。
根据您对 Pythondefaultdict
类型的使用案例,您可能需要在完成创建后冻结字典并将其设为只读。为此,您可以在完成字典填充后设置.default_factory
为None
。这样,您的字典将表现得像一个标准dict
,这意味着您将不会有更多自动生成的默认值。
defaultdict
对比 dict.setdefault()
正如您之前看到的,dict
提供了.setdefault()
,它允许您即时为缺失的键分配值。相反,使用 adefaultdict
可以在初始化容器时预先指定默认值。您可以使用.setdefault()
来分配默认值,如下所示:
>>> d = dict()
>>> d.setdefault('missing_key', [])
[]
>>> d
{'missing_key': []}
在此代码中,您创建了一个常规字典,然后使用它为尚未定义的键.setdefault()
分配一个值 ( []
) missing_key
。
注意:您可以使用.setdefault()
. 与defaultdict
您认为defaultdict
仅接受可调用或None
.
另一方面,如果您使用 adefaultdict
来完成相同的任务,那么每当您尝试访问或修改丢失的密钥时,都会按需生成默认值。请注意,使用defaultdict
,默认值是由您预先传递给类的初始化程序的可调用对象生成的。这是它的工作原理:
>>> from collections import defaultdict
>>> dd = defaultdict(list)
>>> dd['missing_key']
[]
>>> dd
defaultdict(<class 'list'>, {'missing_key': []})
在这里,您首先defaultdict
从collections
. 然后,您创建一个defaultdict
并传递list
给.default_factory
. 当您尝试访问丢失的键时,在defaultdict
内部调用.default_factory()
,它保存对 的引用list
,并将结果值(空的list
)分配给missing_key
。
上面两个示例中的代码执行相同的工作,但该defaultdict
版本可以说更具可读性、用户友好性、Pythonic 和简单明了。
注意:调用内置类型如list
, set
, dict
, str
, int
, orfloat
将返回一个空对象或数字类型的零。
看看下面的代码示例:
>>> list()
[]
>>> set()
set([])
>>> dict()
{}
>>> str()
''
>>> float()
0.0
>>> int()
0
在此代码中,您调用一些没有参数的内置类型,并为数字类型获取一个空对象或零。
最后,使用 adefaultdict
来处理丢失的键可能比使用dict.setdefault()
. 看看下面的例子:
# Filename: exec_time.py
from collections import defaultdict
from timeit import timeit
animals = [('cat', 1), ('rabbit', 2), ('cat', 3), ('dog', 4), ('dog', 1)]
std_dict = dict()
def_dict = defaultdict(list)
def group_with_dict():
for animal, count in animals:
std_dict.setdefault(animal, []).append(count)
return std_dict
def group_with_defaultdict():
for animal, count in animals:
def_dict[animal].append(count)
return def_dict
print(f'dict.setdefault() takes {timeit(group_with_dict)} seconds.')
print(f'defaultdict takes {timeit(group_with_defaultdict)} seconds.')
如果您从系统的命令行运行脚本,那么您将得到如下内容:
$ python3 exec_time.py
dict.setdefault() takes 1.0281260240008123 seconds.
defaultdict takes 0.6704721650003194 seconds.
在这里,您可以使用timeit.timeit()
来衡量的执行时间group_with_dict()
和group_with_defaultdict()
。这些函数执行等效的操作,但第一个使用dict.setdefault()
,第二个使用defaultdict
。时间度量将取决于您当前的硬件,但您可以在此处看到它defaultdict
比dict.setdefault()
. 随着数据集变大,这种差异会变得更加重要。
此外,您需要考虑到创建常规dict
可能比创建defaultdict
. 看看这段代码:
>>> from timeit import timeit
>>> from collections import defaultdict
>>> print(f'dict() takes {timeit(dict)} seconds.')
dict() takes 0.08921320698573254 seconds.
>>> print(f'defaultdict() takes {timeit(defaultdict)} seconds.')
defaultdict() takes 0.14101867799763568 seconds.
这一次,你timeit.timeit()
用来衡量dict
和defaultdict
实例化的执行时间。请注意,创建 adict
几乎是创建 a 的一半时间defaultdict
。如果您考虑到在实际代码中通常defaultdict
只实例化一次,这可能不是问题。
另请注意,默认情况下,timeit.timeit()
将运行您的代码一百万次。这就是定义std_dict
和def_dict
超出group_with_dict()
和group_with_defaultdict()
in范围的原因exec_time.py
。否则,时间测量将通过实例化时间的影响dict
和defaultdict
。
此时,您可能知道何时使用 adefaultdict
而不是常规dict
. 以下是需要考虑的三件事:
-
如果您的代码在很大程度上基于字典并且您一直在处理丢失的键,那么您应该考虑使用.
defaultdict
而不是常规的dict
. -
如果您的字典项需要使用常量默认值初始化,那么您应该考虑使用 a
defaultdict
而不是 adict
。 -
如果您的代码依赖字典来聚合、累加、计数或分组值,并且性能是一个问题,那么您应该考虑使用
defaultdict
.
在决定使用 adict
还是 a时,您可以考虑上述准则defaultdict
。
defaultdict.__missing__()
在幕后,Pythondefaultdict
类型通过调用.default_factory
为缺少的键提供默认值来工作。使这成为可能的机制是.__missing__()
,所有标准映射类型都支持的特殊方法,包括dict
和defaultdict
。
注意:注意.__missing__()
由 自动调用.__getitem__()
以处理丢失的键,.__getitem__()
同时由 Python 自动调用以进行订阅操作,例如d[key]
.
那么,它是如何.__missing__()
工作的呢?如果设置.default_factory
为None
,则以为参数.__missing__()
引发 a 。否则,不带参数调用以提供给定的默认值。这被插入到字典中并最终返回。如果调用引发异常,则该异常将原样传播。KeyError
key
.default_factory
value
key
value
.default_factory
以下代码显示了一个可行的 Python 实现.__missing__()
:
1def __missing__(self, key):
2 if self.default_factory is None:
3 raise KeyError(key)
4 if key not in self:
5 self[key] = self.default_factory()
6 return self[key]
下面是这段代码的作用:
- 在第 1 行,您定义方法及其签名。
- 在第 2 行和第 3 行中,您测试是否
.default_factory
为None
。如果是这样,那么你提出一个KeyError
与key
作为参数。 - 在第 4 行和第 5 行中,您检查
key
字典中是否没有 。如果不是,则调用.default_factory
并将其返回值分配给key
. - 在第 6 行,您
key
按预期返回。
请记住,.__missing__()
映射中的存在对其他查找键的方法的行为没有影响,例如实现运算符的.get()
or 。那是因为只有在字典中找不到请求时才会调用。无论返回或引发什么,然后由 返回或引发。.__contains__()
in
.__missing__()
.__getitem__()
key
.__missing__()
.__getitem__()
既然您已经介绍了 的替代 Python 实现.__missing__()
,那么尝试defaultdict
用一些 Python 代码进行模拟将是一个很好的练习。这就是您将在下一节中执行的操作。
模拟 Pythondefaultdict
类型
在本节中,您将编写一个 Python 类,其行为与defaultdict
. 为此,您将子类化collections.UserDict
,然后添加.__missing__()
. 此外,您需要添加一个名为 的实例属性.default_factory
,它将保存可调用以按需生成默认值。这是一段模拟 Pythondefaultdict
类型的大部分行为的代码:
1import collections
2
3class my_defaultdict(collections.UserDict):
4 def __init__(self, default_factory=None, *args, **kwargs):
5 super().__init__(*args, **kwargs)
6 if not callable(default_factory) and default_factory is not None:
7 raise TypeError('first argument must be callable or None')
8 self.default_factory = default_factory
9
10 def __missing__(self, key):
11 if self.default_factory is None:
12 raise KeyError(key)
13 if key not in self:
14 self[key] = self.default_factory()
15 return self[key]
下面是这段代码的工作原理:
-
在第 1 行中,您导入
collections
以访问UserDict
. -
在第 3 行中,您创建了一个子类化的类
UserDict
。 -
在第 4 行中,您定义了类初始值设定项
.__init__()
。此方法采用一个被default_factory
调用的参数来保存您将用于生成默认值的可调用对象。请注意,default_factory
默认为None
,就像在defaultdict
. 您还需要*args
和**kwargs
来模拟常规dict
. -
在第 5 行,您调用超类
.__init__()
。这意味着,我们在调用UserDict.__init__()
和传递*args
,并**kwargs
给它。 -
在第 6 行,您首先检查是否
default_factory
是有效的可调用对象。在这种情况下,您使用callable(object)
,它是一个内置函数,True
如果object
看起来是可调用的则返回,否则返回False
。此检查可确保您.default_factory()
在需要value
为任何缺失的key
. 然后,您检查是否.default_factory
不是None
。 -
在第 7 行中,您提出 a
TypeError
就像普通人dict
会做的 ifdefault_factory
is 一样None
。 -
在第 8 行中,您初始化
.default_factory
. -
在第 10 行,您定义了
.__missing__()
,正如您之前看到的那样实现。回想一下,当给定的字典不在字典中时,它.__missing__()
会自动调用。.__getitem__()
key
如果您有心情阅读一些C代码,那么您可以查看CPython 源代码中Python类型的完整代码。defaultdict
现在您已经完成了这个类的编码,您可以通过将代码放入一个名为的 Python 脚本中my_dd.py
并从交互式会话中导入它来测试它。下面是一个例子:
>>> from my_dd import my_defaultdict
>>> dd_one = my_defaultdict(list)
>>> dd_one
{}
>>> dd_one['missing']
[]
>>> dd_one
{'missing': []}
>>> dd_one.default_factory = int
>>> dd_one['another_missing']
0
>>> dd_one
{'missing': [], 'another_missing': 0}
>>> dd_two = my_defaultdict(None)
>>> dd_two['missing']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
dd_two['missing']
File "/home/user/my_dd.py", line 10,
in __missing__
raise KeyError(key)
KeyError: 'missing'
在这里,您首先my_defaultdict
从my_dd
. 然后,您创建 的实例my_defaultdict
并传递list
给.default_factory
。如果您尝试通过订阅操作访问密钥,例如dd_one['missing']
,则.__getitem__()
Python 会自动调用。如果键不在字典中,则.__missing__()
调用它,它通过调用生成一个默认值.default_factory()
。
您还可以.default_factory
使用正常的分配操作更改分配给的可调用对象,例如dd_one.default_factory = int
。最后,如果您传递None
给.default_factory
,那么您将KeyError
在尝试检索丢失的密钥时得到 。
注意: a的行为defaultdict
本质上与此 Python 等效项相同。但是,您很快就会注意到您的 Python 实现不会打印为真实defaultdict
的dict
. 您可以通过覆盖.__str__()
和来修改此详细信息.__repr__()
。
您可能想知道为什么在此示例中使用子类collections.UserDict
而不是常规dict
。这样做的主要原因是对内置类型进行子类化可能容易出错,因为内置类型的 C 代码似乎不会始终如一地调用由用户覆盖的特殊方法。
这是一个示例,显示了在子类化时可能面临的一些问题dict
:
>>> class MyDict(dict):
... def __setitem__(self, key, value):
... super().__setitem__(key, None)
...
>>> my_dict = MyDict(first=1)
>>> my_dict
{'first': 1}
>>> my_dict['second'] = 2
>>> my_dict
{'first': 1, 'second': None}
>>> my_dict.setdefault('third', 3)
3
>>> my_dict
{'first': 1, 'second': None, 'third': 3}
在本例中,您创建MyDict
,它是一个子类化 的类dict
。您的实现.__setitem__()
始终将值设置为None
. 如果您创建一个实例MyDict
并将关键字参数传递给它的初始值设定项,那么您会注意到该类没有调用您.__setitem__()
来处理分配。您知道这是因为first
未分配密钥None
。
相比之下,如果您运行像 那样的订阅操作my_dict['second'] = 2
,那么您会注意到它second
设置为None
而不是2
。所以,这一次您可以说订阅操作调用您的自定义.__setitem__()
. 最后,请注意.setdefault()
也不会调用.__setitem__()
,因为您的third
键最终的值为3
。
UserDict
不继承dict
但模拟标准字典的行为。该类有一个dict
名为的内部实例.data
,用于存储字典的内容。UserDict
在创建自定义映射时是一个更可靠的类。如果您使用UserDict
,那么您将避免之前看到的问题。为了证明这一点,回到代码my_defaultdict
并添加以下方法:
1class my_defaultdict(collections.UserDict):
2 # Snip
3 def __setitem__(self, key, value):
4 print('__setitem__() gets called')
5 super().__setitem__(key, None)
在这里,您添加一个.__setitem__()
调用 superclass的自定义.__setitem__()
,它始终将值设置为None
。在您的脚本中更新此代码my_dd.py
并从交互式会话中导入它,如下所示:
>>> from my_dd import my_defaultdict
>>> my_dict = my_defaultdict(list, first=1)
__setitem__() gets called
>>> my_dict
{'first': None}
>>> my_dict['second'] = 2
__setitem__() gets called
>>> my_dict
{'first': None, 'second': None}
在这种情况下,当您实例化my_defaultdict
并传递first
给类初始值设定项时,您的自定义__setitem__()
会被调用。此外,当您为 key 分配一个值时second
,__setitem__()
也会被调用。您现在有一个my_defaultdict
始终调用您的自定义特殊方法的方法。请注意,字典中的所有值都等于None
现在。
将参数传递给 .default_factory
正如您之前看到的,.default_factory
必须设置为一个不带参数并返回值的可调用对象。该值将用于为字典中任何缺失的键提供默认值。即使.default_factory
不应该接受参数,Python 也提供了一些技巧,您可以在需要为其提供参数时使用这些技巧。在本节中,您将介绍两种可用于此目的的 Python 工具:
使用这两个工具,您可以为 Pythondefaultdict
类型增加额外的灵活性。例如,您可以使用一个defaultdict
带有参数的可调用对象初始化 a ,经过一些处理后,您可以使用新参数更新可调用对象,以更改您将从此时起创建的键的默认值。
使用 lambda
将参数传递给的一种灵活方法.default_factory
是使用lambda
. 假设您要创建一个函数以在defaultdict
. 该函数执行一些处理并返回一个值,但您需要传递一个参数才能使该函数正常工作。下面是一个例子:
>>> def factory(arg):
... # Do some processing here...
... result = arg.upper()
... return result
...
>>> def_dict = defaultdict(lambda: factory('default value'))
>>> def_dict['missing']
'DEFAULT VALUE'
在上面的代码中,您创建了一个名为factory()
. 该函数接受一个参数,进行一些处理,并返回最终结果。然后,您创建一个defaultdict
并用于lambda
将字符串传递'default value'
给factory()
. 当您尝试访问丢失的密钥时,将运行以下步骤:
- 字典
def_dict
调用 its.default_factory
,它保存对lambda
函数的引用。 - 该
lambda
函数被调用和返回值从调用的结果factory()
与'default value'
作为参数。
如果您正在使用def_dict
并且突然需要将参数更改为factory()
,那么您可以执行以下操作:
>>> def_dict.default_factory = lambda: factory('another default value')
>>> def_dict['another_missing']
'ANOTHER DEFAULT VALUE'
这一次,factory()
接受一个新的字符串参数 ( 'another default value'
)。从现在开始,如果您尝试访问或修改丢失的键,那么您将获得一个新的默认值,即 string 'ANOTHER DEFAULT VALUE'
。
最后,您可能会遇到需要不同于0
或的默认值的情况[]
。在这种情况下,你也可以使用lambda
,以产生不同的默认值。例如,假设您有一个list
整数,您需要计算每个数字的累积乘积。然后,您可以使用 a defaultdict
with lambda
,如下所示:
>>> from collections import defaultdict
>>> lst = [1, 1, 2, 1, 2, 2, 3, 4, 3, 3, 4, 4]
>>> def_dict = defaultdict(lambda: 1)
>>> for number in lst:
... def_dict[number] *= number
...
>>> def_dict
defaultdict(<function <lambda> at 0x...70>, {1: 1, 2: 8, 3: 27, 4: 64})
在这里,您lambda
用来提供默认值1
。使用这个初始值,您可以计算 中每个数字的累积乘积lst
。请注意,您无法使用 using 获得相同的结果,int
因为返回的默认值int
始终为0
,这对于您需要在此处执行的乘法运算来说不是一个好的初始值。
使用 functools.partial()
functools.partial(func, *args, **keywords)
是一个返回partial
对象的函数。当您使用位置参数 ( args
) 和关键字参数 ( keywords
)调用此对象时,它的行为与您调用func(*args, **keywords)
. 您可以利用 的这种行为partial()
并使用它.default_factory
在 Python 中传递参数defaultdict
。下面是一个例子:
>>> def factory(arg):
... # Do some processing here...
... result = arg.upper()
... return result
...
>>> from functools import partial
>>> def_dict = defaultdict(partial(factory, 'default value'))
>>> def_dict['missing']
'DEFAULT VALUE'
>>> def_dict.default_factory = partial(factory, 'another default value')
>>> def_dict['another_missing']
'ANOTHER DEFAULT VALUE'
在这里,您创建了一个 Pythondefaultdict
并用于partial()
为.default_factory
. 请注意,您还可以更新.default_factory
以使用 callable 的另一个参数factory()
。这种行为可以为您的defaultdict
对象增加很多灵活性。
结论
Pythondefaultdict
类型是 Python 标准库在名为collections
. 该类继承自dict
,其主要附加功能是为缺失的键提供默认值。在本教程中,您学习了如何使用 Pythondefaultdict
类型来处理字典中缺失的键。
您现在可以:
- 创建并使用Python
defaultdict
来处理丢失的键 - 解决与分组、计数和累加操作相关的实际问题
- 了解
defaultdict
和之间的实现差异dict
- 决定何时以及为何使用 Python
defaultdict
而不是标准dict
Pythondefaultdict
类型是一种方便且高效的数据结构,旨在帮助您处理字典中缺少的键。试一试,让你的代码更快、更易读、更 Pythonic!
- 点赞
- 收藏
- 关注作者
评论(0)