从容应对 KeyError:Python 中的 defaultdict 模块

文章目录

  • 参考
  • 描述
  • defaultdict
      • 参数
      • default_factory
  • 原理
      • 魔术方法 \_\_missing\_\_
      • 模仿游戏
      • 单次调用
  • setdefault 还是 defaultdict ?
      • setdefault
      • 对比
      • 结论

参考

项目 描述
Python 标准库 DougHellmann / 刘炽 等
搜索引擎 Bing
Python 官方文档 collections — 容器数据类型

描述

项目 描述
Python 解释器 3.10.6

defaultdict

defaultdict 是 Python 标准库 collections 模块中的一种字典类型,它提供了一种更加便捷的方式来处理字典中的缺失键值对。当我们使用普通的字典对象访问一个不存在的键时,Python 将抛出一个 KeyError 异常。而使用 defaultdict 模块,你将能够通过提供一个可调用对象以在访问一个不存在的键时向用户提供一个默认值并将相关的键值对添加到 defaultdict 字典中。对此,请参考如下示例:

from collections import defaultdict


# 尝试访问一个不存在的键时,defaultdict
# 将调用 int() 来返回一个默认值。
dd = defaultdict(int)
print(dd)

# 尝试访问一个不存在的键
print(dd['a'])

# 对一个不存在的键进行自增操作
dd['b'] += 1
print(dd)

执行效果

defaultdict(<class 'int'>, {})
0
defaultdict(<class 'int'>, {'a': 0, 'b': 1})

参数

defaultdict(default_factory=None, /, [...])

其中:

项目 描述
default_factory 在尝试访问一个不存在的键时,将执行 default_factory 对应的可调用对象以返回一个默认值。
[…] 除去第一个位置参数外,其他参数都将传递给 dict 构造函数用于为 defaultdict 字典添加键值对。

注:

Python 3 中,/ 符号用于分隔位置参数和关键字参数,它出现在参数列表中,表示该符号之前的参数均为位置参数。对此,请参考如下示例:

from collections import defaultdict


dd = defaultdict(default_factory={'a': 1, 'b': 3})
print(dd)
print(dd['a'])

执行效果

default_factory 仅能通过位置参数的形式进行传递,由于我们使用了关键字参数。因此 default_factory 将使用其默认值 None 。而 default_factory={‘a’: 1, ‘b’: 3} 也就成了第二个参数实际接收到的数据。

defaultdict(None, {'default_factory': {'a': 1, 'b': 3}})

产生错误信息

Traceback (most recent call last):
  File "C:\main.py", line 6, in <module>
    print(dd['a'])
KeyError: 'a'

default_factory

default_factory 可以是 Python 内置的可调用对象外,也可以是我们自定义的可调用对象。相比 Python 内置的可调用对象,使用自定义可调用对象可以对返回值进行更细腻、更具有针对性的控制。对此,请参考如下示例:

from collections import defaultdict


def default_generator():
    return 'Hello World'

dd = defaultdict(default_generator)
print(dd['a'])

执行效果

Hello World

原理

魔术方法 __missing__

__missing__() 是 Python 中的一个特殊方法,用于处理通过键访问字典中的值时键不存在时的情况。
当我们使用字典的索引来访问一个不存在的键时,Python 将会调用特殊方法 __missing__() 来尝试返回一个合适的值。若未实现 __missing__() 方法,Python 将会抛出 KeyError 异常。对此,请参考如下示例:

# 创建一个字典对象,该对象继承自 Python 内置的 dict 对象
class MyDict(dict):
    def __missing__(self, key):
        return 0

# 实例化 MyDict() 对象
myDict = MyDict()
# 尝试访问 myDict 对象中不存在的键 a
print(myDict['a'])

执行效果

0

模仿游戏

defaultdict 正是因为实现了魔术方法 _missing_ ,所以才能够在用户尝试访问一个不存在的键时进行适当的处理而不至于引发 KeyError 异常。
我们可以通过实现 _missing_ 来模仿 defaultdict 。对此,请参考如下示例:

# MyDefaultdict 类继承 dict 以模仿原生字典
class MyDefaultdict(dict):
    def __init__(self, default_factory=None):
        # 执行 default_factory 可执行对象以获得默认值
        self.result = default_factory()

    def __missing__(self, key):
        # 键 = 默认值
        self[key] = self.result
        return self.result


# 在尝试访问一个不存在的键时,调用 str() 以获得默认值
myDefaultdict = MyDefaultdict(str)
print(myDefaultdict)

print(myDefaultdict['a'])
myDefaultdict['b'] += 'Hello World'
print(myDefaultdict)

执行效果

{}

{'a': '', 'b': 'Hello World'}

单次调用

再深入了解 default_factory 参数前,请先观察如下示例:

from collections import defaultdict


# 创建一个生成器(通过生成器推导式)
arr = (i for i in range(1, 100))
print(arr)

# 使用生成器对象的 __next__() 方法来生成默认值
dd = defaultdict(arr.__next__)
for c in 'Hello World':
    print(dd['c'])

执行效果

<generator object <genexpr> at 0x000001D4A4DBE3B0>
1
1
1
1
1
1
1
1
1
1
1

分析

在观察上述示例后,想必你也产生了疑惑。为什么访问不存在的键时,获取到的值均为第一次调用 default_factory 参数所对应的可调用对象的值。这是因为在创建 defaultdict 对象时,defaultdict 就已经调用了 default_factory 对象获取默认值。在该 defaultdict 对象的生命中,都将使用该值作为默认值,而不会在每次访问不存在的键时都去调用 default_factory 对象。

所以在上一个示例中,对 defaultdict 进行模仿时,我们使用的是:

# MyDefaultdict 类继承 dict 以模仿原生字典
class MyDefaultdict(dict):
    def __init__(self, default_factory=None):
        # 执行 default_factory 可执行对象以获得默认值
        self.result = default_factory()

    def __missing__(self, key):
        # 键 = 默认值
        self[key] = self.result
        return self.result

而不是(第二种实现):

class MyDefaultdict(dict):
    def __init__(self, default_factory=None):
        # 将 default_factory 的地址传递给 self.default_factory
        self.default_factory = default_factory

    def __missing__(self, key):
        # 在每一次访问不存在的键时都将调用 default_factory()
        # 来获取返回值
        result = self.default_factory()
        self[key] = result
        return result

如果使用 第二种实现,你将看到另一种景观。对此,请参考如下示例:

class MyDefaultdict(dict):
    def __init__(self, default_factory=None):
        # 将 default_factory 的地址传递给 self.default_factory
        self.default_factory = default_factory

    def __missing__(self, key):
        # 在每一次访问不存在的键时都将调用 default_factory()
        # 来获取返回值
        result = self.default_factory()
        self[key] = result
        return result


# 创建一个生成器(通过生成器推导式)
arr = (i for i in range(1, 100))
print(arr)

# 使用生成器对象的 __next__() 方法来生成默认值
myDefaultdict = MyDefaultdict(arr.__next__)
for c in 'Hello World':
    print(myDefaultdict[c])

执行效果

<generator object <genexpr> at 0x000001C0EDE99A10>
1
2
3
3
4
5
6
4
7
3
8

在正常情况下,建议使用 defaultdict 而不是此例中的第二种实现。defaultdict 提供了一个确定的默认值,有利于代码的维护与阅读。而第二种实现可能会提供不同的默认值,这可能会引起混乱。

setdefault 还是 defaultdict ?

setdefault

Python 的内置字典中提供了 setdefault 方法用于实现与 defaultdict 类似的功能。
在 Python 内置的字典对象中,setdefault 方法是一个常用的方法之一,可以用于获取字典中指定键对应的值。如果该键不存在,则将该键和指定的默认值添加到字典中并将默认值进行返回。

setdefault 方法的语法如下:

dict.setdefault(key, default=None)

举个栗子

# 创建一个空字典
d = dict()

# 尝试访问键 a
result = d.setdefault('a', [])
# 由于键 a 不存在,因此返回默认值
# 并将该键与默认值添加到字典中。
print(result)
print(d)

result.append('Hello World')
print(d)

执行效果

[]
{'a': []}
{'a': ['Hello World']}

对比

setdefaultdefaultdict 都可以用来设置字典中键的默认值,但它们在实现上有所不同。

  1. defaultdict 接受一个可调用对象(default_factory 参数的值 必须 为一个可调用对象,否则将引发异常),在需要默认值时将执行该可调用对象以获取默认值。而 setdefault 方法同样可以接受一个可调用对象,但在需要默认值时会直接将该可调用对象作为默认值进行返回。对此,请参考如下示例:
# 创建一个空字典
d = dict()

# 尝试获取键 a 的值
d.setdefault('a', int)
print(d)

执行效果

{'a': <class 'int'>}
  1. 使用 defaultdict 可以创建一个具有默认值的字典,而 dict 在可能需要使用到默认值的地方都需要使用到 setdefault 来设置默认值。这样不利于代码的维护与阅读。

结论

defaultdictsetdefault 两者并无优劣之分,在不同的场景中使用合适的工具才是重中之重。

你可能感兴趣的:(Python,python,Collections,Python,标准库,defaultdict,setdefault)