我有一个基本上相当于嵌套字典的数据结构。假设它看起来像这样:
{'new jersey': {'mercer county': {'plumbers': 3, 'programmers': 81}, 'middlesex county': {'programmers': 81, 'salesmen': 62}}, 'new york': {'queens county': {'plumbers': 9, 'salesmen': 36}}}
现在,维护和创建它非常痛苦;每次我有一个新的州/县/专业时,我都必须通过令人讨厌的 try/catch 块创建下层词典。此外,如果我想遍历所有值,我必须创建烦人的嵌套迭代器。
我也可以使用元组作为键,如下所示:
{('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}
这使得对值的迭代变得非常简单和自然,但是在语法上做聚合和查看字典的子集(例如,如果我只想逐个状态)会更加痛苦。
基本上,有时我想将嵌套字典视为平面字典,有时我想将其视为复杂的层次结构。我可以将这一切都包装在一个类中,但似乎有人可能已经这样做了。或者,似乎有一些非常优雅的句法结构可以做到这一点。
我怎样才能做得更好?
附录:我知道,setdefault()但它并没有真正实现干净的语法。此外,您创建的每个子词典仍然需要setdefault()手动设置。
setdefault()
在 Python 中实现嵌套字典的最佳方法是什么?
这是一个坏主意,不要这样做。相反,请使用常规字典并使用dict.setdefaultwhere apropos,因此当在正常使用下缺少键时,您会得到预期的KeyError. 如果您坚持要采取这种行为,请按以下步骤射击自己:
dict.setdefault
KeyError
__missing__在子类上实现dict以设置并返回一个新实例。
__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]在赋值的左边,所以这里没有递归。)
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,不幸的是,我们不能在设置时返回它。)
Vividict
请注意,这些语义与最受好评的答案相同,但只有一半的代码行 - 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__将其类的新实例设置为键并返回它是一个更好的解决方案。
虽然提问者认为这不干净,但我觉得它比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__为此目的而制作的 )相比,它要优越得多。
__getitem__
__missing__在子类上实现dict以设置和返回新实例比替代方案稍微困难一些,但具有以下好处
并且由于它比修改更简单且性能更高__getitem__,因此应该首选该方法。
尽管如此,它也有缺点:
因此,我个人更喜欢setdefault其他解决方案,并且在我需要这种行为的每种情况下都有。
setdefault