使用 Python defaultdict 类型处理丢失的键

举报
Yuchuan 发表于 2021/12/23 19:31:37 2021/12/23
【摘要】 Pythondefaultdict类型是 Python 标准库在名为collections. 该类继承自dict,其主要附加功能是为缺失的键提供默认值。在本教程中,您学习了如何使用 Pythondefaultdict类型来处理字典中缺失的键。

目录

使用 Python字典时可能会遇到的一个常见问题是尝试访问或修改字典中不存在的键。这将引发 aKeyError并中断您的代码执行。为了处理这些情况,标准库提供了 Pythondefaultdict类型,这是一个类似字典的类,在collections.

Pythondefaultdict类型的行为几乎与常规 Python 字典完全相同,但是如果您尝试访问或修改缺少的键,defaultdict则将自动创建该键并为其生成默认值。这defaultdict为处理字典中丢失的键提供了一个有价值的选择。

在本教程中,您将学习:

  • 如何使用 Pythondefaultdict类型处理字典中缺失的键
  • 何时以及为何使用 Pythondefaultdict而不是常规dict
  • 如何使用 adefaultdict进行分组计数累加操作

掌握这些知识后,您将能够更好地defaultdict在日常编程挑战中有效地使用 Python类型。

为了充分利用本教程,您应该事先了解 Python词典是什么以及如何使用它们。如果您需要焕然一新,请查看以下资源:

处理字典中的缺失键

使用 Python 字典时可能面临的一个常见问题是如何处理丢失的键。如果您的代码很大程度上基于字典,或者您一直在动态创建字典,那么您很快就会注意到处理频繁的KeyError异常可能会非常烦人,并且会给您的代码增加额外的复杂性。使用 Python 字典,您至少有四种可用的方法来处理丢失的键:

  1. 利用 .setdefault()
  2. 利用 .get()
  3. 使用key in dict成语
  4. 使用 atryexcept

Python文档说明.setdefault().get()如下:

setdefault(key[, default])

如果key在字典中,则返回其值。如果不是,则插入key值为default并返回defaultdefault默认为None.

get(key[, default])

返回keyifkey在字典中的值, else default。如果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使用 atryexcept块来处理异常。考虑下面的一段代码:

>>>
>>> a_dict = {}
>>> try:
...     # Do something with 'key'...
...     a_dict['key']
... except KeyError:
...     a_dict['key'] = 'default value'
...
>>> a_dict
{'key': 'default value'}

上面示例中的tryandexcept块会在KeyError您尝试访问丢失的密钥时捕获。在except子句中,您创建key并为其分配一个'default value'

注意:如果缺少的键在您的代码中并不常见,那么您可能更喜欢使用 atryexcept块(EAFP 编码风格)来捕获KeyError异常。这是因为代码不会检查每个键是否存在,并且只处理少数异常(如果有)。

另一方面,如果缺少键在您的代码中很常见,那么条件语句(LBYL 编码风格)可能是更好的选择,因为检查键的成本比处理频繁异常的成本更低。

到目前为止,您已经学会了如何使用dictPython 提供的工具处理丢失的键。但是,您在此处看到的示例非常冗长且难以阅读。它们可能不像您想要的那么简单。这就是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类型做了两件事:

  1. 它覆盖.__missing__().
  2. 它添加了.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类型

有时候,你会使用内置集合一个可变的(一个listdictset)在你的Python字典值。在这些情况下,您需要在第一次使用前初始化密钥,否则您会得到一个KeyError. 您可以手动执行此过程,也可以使用 Python 自动执行此过程defaultdict。在本节中,您将学习如何使用 Pythondefaultdict类型来解决一些常见的编程问题:

  • 将集合中的项目分组
  • 计算集合中的项目
  • 累积集合中的值

您将介绍一些使用listsetintfloat以用户友好且高效的方式执行分组、计数和累加操作的示例。

分组项目

Pythondefaultdict类型的典型用途是设置.default_factorylist然后构建一个将键映射到值列表的字典。有了这个defaultdict,如果您尝试访问任何丢失的键,则字典将运行以下步骤:

  1. 调用 list()创建一个新的空list
  2. 插入空的list使用缺少键进入词典key
  3. 返回对那个的引用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.

您可以使用defaultdictwithlist对序列或集合中的项目进行分组。假设您已从公司的数据库中检索到以下数据:

部门 员工姓名
Sales John Doe
Sales Martin Smith
Accounting Jane Doe
Marketing Elizabeth Smith
Marketing Adam Doe

有了这些数据,您创建初始listtuple像下列对象:

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']})

在此示例中,您将使用defaultdictwith.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 setas 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_factoryset集合唯一对象的集合,这意味着您不能创建set具有重复项的集合。这是集合的一个非常有趣的特性,它保证您在最终字典中不会有重复的项目。

计数项目

如果您设置.default_factoryint,那么您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_factoryintint()不带参数调用时,返回值为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})

在上面的代码中,您创建了一个defaultdictwith .default_factoryset to int。这将任何给定键的默认值设置为0。然后,您使用for循环遍历字符串 s并使用增强赋值操作1在每次迭代中添加到计数器。ddwill的键是 中的字母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和得到如下处理数据listtuple对象:

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带有floatas的 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 行中,您导入 Pythondefaultdict类型。
  • 在第 3 行中,您创建了一个设置为的defaultdict对象。.default_factoryfloat
  • 在第 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()以获取有效的属性列表dictdefaultdict。然后,您使用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_factoryNone

如果您在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 ddis a defaultdictand keyis a missing key, then dd[key]will call .default_factoryto 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从更改liststr。现在,每当您尝试访问丢失的密钥时,您的默认值将是一个空字符串 ( '')。

根据您对 Pythondefaultdict类型的使用案例,您可能需要在完成创建后冻结字典并将其设为只读。为此,您可以在完成字典填充后设置.default_factoryNone。这样,您的字典将表现得像一个标准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': []})

在这里,您首先defaultdictcollections. 然后,您创建一个defaultdict并传递list.default_factory. 当您尝试访问丢失的键时,在defaultdict内部调用.default_factory(),它保存对 的引用list,并将结果值(空的list)分配给missing_key

上面两个示例中的代码执行相同的工作,但该defaultdict版本可以说更具可读性、用户友好性、Pythonic 和简单明了。

注意:调用内置类型如listsetdictstrint, 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。时间度量将取决于您当前的硬件,但您可以在此处看到它defaultdictdict.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()用来衡量dictdefaultdict实例化的执行时间。请注意,创建 adict几乎是创建 a 的一半时间defaultdict。如果您考虑到在实际代码中通常defaultdict只实例化一次,这可能不是问题。

另请注意,默认情况下,timeit.timeit()将运行您的代码一百万次。这就是定义std_dictdef_dict超出group_with_dict()group_with_defaultdict()in范围的原因exec_time.py。否则,时间测量将通过实例化时间的影响dictdefaultdict

此时,您可能知道何时使用 adefaultdict而不是常规dict. 以下是需要考虑的三件事:

  1. 如果您的代码在很大程度上基于字典并且您一直在处理丢失的键,那么您应该考虑使用.defaultdict而不是常规的dict.

  2. 如果您的字典项需要使用常量默认值初始化,那么您应该考虑使用 adefaultdict而不是 a dict

  3. 如果您的代码依赖字典来聚合、累加、计数或分组值,并且性能是一个问题,那么您应该考虑使用defaultdict.

在决定使用 adict还是 a时,您可以考虑上述准则defaultdict

defaultdict.__missing__()

在幕后,Pythondefaultdict类型通过调用.default_factory为缺少的键提供默认值来工作。使这成为可能的机制是.__missing__(),所有标准映射类型都支持的特殊方法,包括dictdefaultdict

注意:注意.__missing__()由 自动调用.__getitem__()以处理丢失的键,.__getitem__()同时由 Python 自动调用以进行订阅操作,例如d[key].

那么,它是如何.__missing__()工作的呢?如果设置.default_factoryNone,则以为参数.__missing__()引发 a 。否则,不带参数调用以提供给定的默认值。这被插入到字典中并最终返回。如果调用引发异常,则该异常将原样传播。KeyErrorkey.default_factoryvaluekeyvalue.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_factoryNone。如果是这样,那么你提出一个KeyErrorkey作为参数。
  • 在第 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 行中,您提出 aTypeError就像普通人dict会做的 if default_factoryis 一样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_defaultdictmy_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 实现不会打印为真实defaultdictdict. 您可以通过覆盖.__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 工具:

  1. lambda
  2. functools.partial()

使用这两个工具,您可以为 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(). 当您尝试访问丢失的密钥时,将运行以下步骤:

  1. 字典def_dict调用 its .default_factory,它保存对lambda函数的引用。
  2. 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 defaultdictwith 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类型来处理字典中缺失的键。

您现在可以:

  • 创建并使用Pythondefaultdict来处理丢失的键
  • 解决与分组、计数和累加操作相关的实际问题
  • 了解defaultdict和之间的实现差异dict
  • 决定何时以及为何使用 Pythondefaultdict而不是标准dict

Pythondefaultdict类型是一种方便且高效的数据结构,旨在帮助您处理字典中缺少的键。试一试,让你的代码更快、更易读、更 Pythonic!

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。