如何在Python中实现嵌套字典?
这是一个不好的思路,不要这样做。相反,使用普通字典,并在适当时使用dict.setdefault
,这样在正常使用下丢失键时您将获得预期的KeyError
。如果您坚持要获取此行为,则可以这样做:
在dict
子类上实现__missing__
以设置并返回新实例。
自Python 2.5以来,这种方法已经可用(并记录),并且非常有价值,因为它可以像普通字典一样漂亮地打印,而不是autovivified defaultdict的丑陋打印方式:
class Vividict(dict):
def __missing__(self, key):
value = self[key] = type(self)()
return value
(Note
self[key]
is on the left-hand side of assignment, so there's no recursion here.)
并且假设你有一些数据:
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
类的实例。(返回值赋值很有用,因为它避免了我们在字典上额外调用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
使用演示
以下只是一个例子,展示了如何轻松使用此字典来创建一个嵌套的字典结构。这可以快速创建一个树形结构,您可以按照需要将其深度扩展。
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 = {}
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笔记本中),那么性能并不重要-在这种情况下,我会选择Vividict以便输出更易读。与AutoVivification对象相比(它使用
__getitem__
而不是为此目的创建的
__missing__
),它要优秀得多。
结论
在子类化的
dict
上实现
__missing__
以设置和返回新实例比其他方法略微困难,但具有以下优点:
易于实例化
易于填充数据
易于查看数据
由于它比修改
__getitem__
方法更简单且更高效,因此应优先考虑使用该方法。
然而,它也有缺点:
坏的查找将会默默失败。
错误的查找结果将会保留在字典中。
因此,我个人更喜欢使用
setdefault
而不是其他解决方案,并且在我需要这种行为的每种情况下都使用它。
Vividict
指定有限深度和叶子类型?例如,对于一个可以用d['primary']['secondary']['tertiary'].append(element)
填充的字典的字典的字典的列表,可以指定3
和list
。我可以为每个深度定义 3 种不同的类,但我希望找到更清晰的解决方案。 - Eric Duminild['primary']['secondary'].setdefault('tertiary', []).append('element')
- ?? 感谢夸奖,但让我诚实地说 - 我从来没有使用过__missing__
- 我总是使用setdefault
。我应该更新我的结论/介绍... - Russia Must Remove Putinsetdefault
失败。看起来 Python 中没有任何结构可以像描述的那样提供真正的活化功能。我不得不采用两种声明方法,一种是get_nested
,另一种是set_nested
,它们接受字典引用和嵌套属性列表。 - nehem