在 Python 中实现嵌套字典的最佳方法是什么?
这是一个坏主意,不要这样做。相反,请使用常规字典并使用dict.setdefault
where apropos,因此当在正常使用下缺少键时,您会得到预期的KeyError
. 如果您坚持要采取这种行为,请按以下步骤射击自己:
__missing__
在子类上实现dict
以设置并返回一个新实例。
这种方法自 Python 2.5 起就可用(并记录在案) ,并且(对我特别有价值)它像普通的 dict 一样漂亮地打印出来,而不是自动生成 defaultdict 的丑陋打印:
class Vividict(dict):
def __missing__(self, key):
value = self[key] = type(self)() # retain local pointer to value
return value # faster to return than dict lookup
(注意self[key]
在赋值的左边,所以这里没有递归。)
并说你有一些数据:
data = {('new jersey', 'mercer county', 'plumbers'): 3,
('new jersey', 'mercer county', 'programmers'): 81,
('new jersey', 'middlesex county', 'programmers'): 81,
('new jersey', 'middlesex county', 'salesmen'): 62,
('new york', 'queens county', 'plumbers'): 9,
('new york', 'queens county', 'salesmen'): 36}
这是我们的使用代码:
vividict = Vividict()
for (state, county, occupation), number in data.items():
vividict[state][county][occupation] = number
现在:
>>> import pprint
>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36}}}
批评
对这种容器的批评是,如果用户拼错了一个键,我们的代码可能会默默地失败:
>>> vividict['new york']['queens counyt']
{}
此外,现在我们的数据中有一个拼写错误的县:
>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36},
'queens counyt': {}}}
解释:
Vividict
每当一个键被访问但丢失时,我们只是提供我们类的另一个嵌套实例。(返回赋值是有用的,因为它避免了我们额外调用 dict 上的 getter,不幸的是,我们不能在设置时返回它。)
请注意,这些语义与最受好评的答案相同,但只有一半的代码行 - nosklo 的实现:
class AutoVivification(dict):
"""Implementation of perl's autovivification feature."""
def __getitem__(self, item):
try:
return dict.__getitem__(self, item)
except KeyError:
value = self[item] = type(self)()
return value
使用示范
下面只是一个示例,说明如何轻松使用此 dict 动态创建嵌套的 dict 结构。这可以快速创建一个层次结构的树结构,就像你想去的一样深。
import pprint
class Vividict(dict):
def __missing__(self, key):
value = self[key] = type(self)()
return value
d = Vividict()
d['foo']['bar']
d['foo']['baz']
d['fizz']['buzz']
d['primary']['secondary']['tertiary']['quaternary']
pprint.pprint(d)
哪个输出:
{'fizz': {'buzz': {}},
'foo': {'bar': {}, 'baz': {}},
'primary': {'secondary': {'tertiary': {'quaternary': {}}}}}
正如最后一行所示,它打印得很漂亮,可以进行人工检查。但是,如果您想直观地检查您的数据,实施__missing__
将其类的新实例设置为键并返回它是一个更好的解决方案。
其他替代方案,用于对比:
dict.setdefault
虽然提问者认为这不干净,但我觉得它比Vividict
我自己更可取。
d = {} # or dict()
for (state, county, occupation), number in data.items():
d.setdefault(state, {}).setdefault(county, {})[occupation] = number
现在:
>>> pprint.pprint(d, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36}}}
拼写错误会大声失败,并且不会使我们的数据因错误信息而混乱:
>>> d['new york']['queens counyt']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'queens counyt'
此外,我认为 setdefault 在循环中使用时效果很好,并且您不知道您将获得什么键,但是重复使用变得非常繁琐,我认为没有人会想要跟上以下内容:
d = dict()
d.setdefault('foo', {}).setdefault('bar', {})
d.setdefault('foo', {}).setdefault('baz', {})
d.setdefault('fizz', {}).setdefault('buzz', {})
d.setdefault('primary', {}).setdefault('secondary', {}).setdefault('tertiary', {}).setdefault('quaternary', {})
另一个批评是 setdefault 无论是否使用都需要一个新实例。然而,Python(或至少 CPython)在处理未使用和未引用的新实例方面相当聪明,例如,它重用内存中的位置:
>>> id({}), id({}), id({})
(523575344, 523575344, 523575344)
一个自动激活的默认字典
这是一个看起来很整洁的实现,并且在您没有检查数据的脚本中使用将与实现一样有用__missing__
:
from collections import defaultdict
def vivdict():
return defaultdict(vivdict)
但是,如果您需要检查您的数据,以相同方式填充数据的自动激活 defaultdict 的结果如下所示:
>>> d = vivdict(); d['foo']['bar']; d['foo']['baz']; d['fizz']['buzz']; d['primary']['secondary']['tertiary']['quaternary']; import pprint;
>>> pprint.pprint(d)
defaultdict(<function vivdict at 0x17B01870>, {'foo': defaultdict(<function vivdict
at 0x17B01870>, {'baz': defaultdict(<function vivdict at 0x17B01870>, {}), 'bar':
defaultdict(<function vivdict at 0x17B01870>, {})}), 'primary': defaultdict(<function
vivdict at 0x17B01870>, {'secondary': defaultdict(<function vivdict at 0x17B01870>,
{'tertiary': defaultdict(<function vivdict at 0x17B01870>, {'quaternary': defaultdict(
<function vivdict at 0x17B01870>, {})})})}), 'fizz': defaultdict(<function vivdict at
0x17B01870>, {'buzz': defaultdict(<function vivdict at 0x17B01870>, {})})})
这个输出很不雅,结果很不可读。通常给出的解决方案是递归地转换回字典以进行手动检查。这个重要的解决方案留给读者作为练习。
表现
最后,让我们看看性能。我正在减去实例化的成本。
>>> import timeit
>>> min(timeit.repeat(lambda: {}.setdefault('foo', {}))) - min(timeit.repeat(lambda: {}))
0.13612580299377441
>>> min(timeit.repeat(lambda: vivdict()['foo'])) - min(timeit.repeat(lambda: vivdict()))
0.2936999797821045
>>> min(timeit.repeat(lambda: Vividict()['foo'])) - min(timeit.repeat(lambda: Vividict()))
0.5354437828063965
>>> min(timeit.repeat(lambda: AutoVivification()['foo'])) - min(timeit.repeat(lambda: AutoVivification()))
2.138362169265747
基于性能,dict.setdefault
效果最好。如果您关心执行速度,我强烈推荐它用于生产代码。
如果您需要将其用于交互式使用(也许在 IPython 笔记本中),那么性能并不重要 - 在这种情况下,我会使用 Vividict 来提高输出的可读性。与 AutoVivification 对象(使用__getitem__
而不是__missing__
为此目的而制作的 )相比,它要优越得多。
结论
__missing__
在子类上实现dict
以设置和返回新实例比替代方案稍微困难一些,但具有以下好处
并且由于它比修改更简单且性能更高__getitem__
,因此应该首选该方法。
尽管如此,它也有缺点:
- 错误的查找会默默地失败。
- 错误的查找将保留在字典中。
因此,我个人更喜欢setdefault
其他解决方案,并且在我需要这种行为的每一种情况下都有。