在Python中实现嵌套字典的最佳方法是什么?
这是个坏主意,别这么做。相反,使用普通的字典和字典。因此,当正常使用下缺少键时,您将获得预期的KeyError。如果你坚持这种行为,下面是搬起石头砸自己的脚的方法:
在dict子类上实现__missing__来设置并返回一个新实例。
这种方法自Python 2.5以来就已经可用(并有文档),并且(对我来说特别有价值)它像普通的dict一样漂亮地打印,而不是像autovivified 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']
{}
另外,现在我们的数据中有一个拼错的county:
>>> 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': {}}}
解释:
我们只是提供了类的另一个嵌套实例,每当一个键被访问但缺失时。(返回赋值是有用的,因为它避免了我们额外调用dict上的getter,不幸的是,我们不能在它被设置时返回它。)
注意,这些和被点赞最多的答案是相同的语义,但是只有一半的代码行——nosklo的实现:
类AutoVivification (dict类型):
"" perl的自动激活功能的实现。"""
Def __getitem__(self, item):
试一试:
返回dict类型。__getitem__(自我,项)
除了KeyError:
Value = self[item] = type(self)()
返回值
使用说明
下面是一个示例,说明如何轻松地使用这个字典创建一个嵌套的字典结构。这可以快速创建您想要深入的层次树结构。
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
尽管提问者认为这个不干净,但我觉得它比我自己的livelict更可取。
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)
一个自动激活的defaultdict
这是一个整洁的实现,在你没有检查数据的脚本中使用与实现__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笔记本中),那么性能并不重要——在这种情况下,为了输出的可读性,我会选择vivid。与AutoVivification对象(它使用__getitem__而不是__missing__,后者是为此目的而创建的)相比,它要优越得多。
结论
在子类dict上实现__missing__来设置并返回一个新实例比其他方法稍微困难一些,但具有的优点
简单的实例化
简单的数据填充
轻松查看数据
因为它比修改__getitem__更简单,性能更好,所以应该优先使用该方法。
然而,它也有缺点:
错误的查找将会无声地失败。
错误的查找将保留在字典中。
因此,与其他解决方案相比,我个人更喜欢使用setdefault,并且在需要这种行为的每种情况下都使用它。