30.2.7. 用defaultdict处理内部状态中缺失的元素

如果字典不是自己创建的,那么对其中缺失的键可以考虑用四种办法解决。

在这四种办法中,get方案要胜过利用in表达式和KeyError异常来解决的那两种方案,对于某些用例,我们可能觉得setdefault应该是代码最简短的办法。例如,笔者要记录自己去过哪些国家,还要记录在每个国家到过哪些城市。那可以用这样一个字典,把国家名称与包含城市名称的集合。

visits = {
    'Mexico': {'Tulum', 'Puerto Vallarta'},
    'Japan': {'Hakone'},
}

In [2]: visits.setdefault("France",set()).add("Arles")

In [3]: visits
Out[3]:
{'Mexico': {'Puerto Vallarta', 'Tulum'},
 'Japan': {'Hakone'},
 'France': {'Arles'}}
if (japan := visits.get('Japan')) is None:       # 这种代码就长多了
    visits['Japan'] = japan = set()
japan.add('Kyoto')

我们写这样一个类,把刚才那个范例逻辑封装到辅助方法中,使用户可以调用该方法啦访问字典中保存的动态内部状态。

In [6]: class Visits:
   ...:     def __init__(self):
   ...:         self.data = {}
   ...:
   ...:     def add(self, country, city):
   ...:         city_set = self.data.setdefault(country, set())
   ...:         city_set.add(city)


In [7]: visits = Visits()
   ...: visits.add('Russia', 'Yekaterinburg')
   ...: visits.add('Tanzania', 'Zanzibar')
   ...: print(visits.data)


{'Russia': {'Yekaterinburg'}, 'Tanzania': {'Zanzibar'}}

问题是,Visits.add方法还是写得不够理想,因为它还是调用了setdefault方法。这种写法也不够高效,因为每次调用add方法时,无论country参数所指定的国家名称是否存在,都必须构建新的set实例。 Python提供了defaultdict类,能轻松地实现出刚才那套逻辑。它会在键缺失的情况下,自动添加这个键以及键所对应的默认值。我们只需要在构造这种字典时提供一个函数即可。 每次发现键不存在时,该字典都会调用这个函数返回一份新的默认值。

In [9]: from collections import defaultdict

   ...: class Visits:
   ...:     def __init__(self):
   ...:         self.data = defaultdict(set)
   ...:
   ...:     def add(self, country, city):
   ...:         self.data[country].add(city)

   ...: visits = Visits()
   ...: visits.add('England', 'Bath')
   ...: visits.add('England', 'London')
   ...: print(visits.data)


defaultdict(<class 'set'>, {'England': {'Bath', 'London'}})

要点:

如果你管理的字典可能需要添加任意的键,那么应该考虑能否用内置的collections模块中的defaultdict实例来解决问题。

如果这种键名比较随意的字典是别人传给你的,你无法把它创建成defaultdict,那么应该考虑通过get方法访问其中的键值。

然而,在个别情况下,也可以考虑改用setdefault方法,因为那样写更短。