小编典典

实现嵌套字典的最佳方法是什么?

all

我有一个基本上相当于嵌套字典的数据结构。假设它看起来像这样:

{'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()手动设置。


阅读 62

收藏
2022-06-15

共1个答案

小编典典

在 Python 中实现嵌套字典的最佳方法是什么?

这是一个坏主意,不要这样做。相反,请使用常规字典并使用dict.setdefaultwhere
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其他解决方案,并且在我需要这种行为的每种情况下都有。

2022-06-15