多层级可变深度的默认字典?

89
我有一个类似如下的大型列表:

I have a large list like:

[A][B1][C1]=1
[A][B1][C2]=2
[A][B2]=3
[D][E][F][G]=4

我想建立一个多级字典,类似于:

A
--B1
-----C1=1
-----C2=1
--B2=3
D
--E
----F
------G=4
我知道如果使用递归的defaultdict,我可以写成table[A][B1][C1]=1table[A][B2]=2,但是这仅适用于硬编码插入语句的情况。在解析列表时,我不知道需要多少个[]来提前调用table[key1][key2][...]

强相关:https://dev59.com/UmQn5IYBdhLWcg3wzZ76 - cs95
10个回答

216

您甚至不需要定义一个类就可以完成它:

from collections import defaultdict

nested_dict = lambda: defaultdict(nested_dict)
nest = nested_dict()

nest[0][1][2][3][4][5] = 6

15
太好了!但是如果我想让叶子节点通过标准的(int、list等)工厂函数初始化怎么办?比如,我想这样做:table[0][1][2][3][4][5] += 1 - rikb
1
有没有一种使用内置字典和 .get() 方法来实现相同功能的方法? - Aleksandr Levchuk
1
类l(字典):missing=lambda a,b:a.setdefault(b,l()),然后继续从table=l()。 - Hugo Walter
1
PyCharm提示违反PEP 8规范:“不要分配lambda表达式使用def”。有没有通过函数来解决警告的方法? - NaturalBornCamper
4
def nested_dict(): return defaultdict(nested_dict) 但我喜欢lambda版本更好。它看起来有点更加神秘;-) - Hugo Walter
显示剩余3条评论

20

你的例子表明,在任何层级都可以有一个值,以及子元素的字典。这被称为,有许多可用的实现方法。这是其中之一:

from collections import defaultdict
class Tree(defaultdict):
    def __init__(self, value=None):
        super(Tree, self).__init__(Tree)
        self.value = value

root = Tree()
root.value = 1
root['a']['b'].value = 3
print root.value
print root['a']['b'].value
print root['c']['d']['f'].value

输出:

1
3
None

您可以通过将输入写成 JSON 格式,并使用 json.load 将其读取为嵌套字典结构来实现类似的操作。


我认为value结构在所提出的问题中是不必要的。只需删除对value的引用,直接将值分配给字典键即可。 - Jason R. Coombs
+1:虽然 value 参数/属性并不是必需的。 - martineau
4
@Martineau @Jason。value实例变量是必需的,否则当您直接将节点分配时就会丢失子项(请参见我对Jason优雅解决方案的评论)。干预__setitem__会提供更加健壮的解决方案,但对于简单的要求来说,这将是一个过于复杂的解决方案。 - Apalala
我不太清楚如何修改其他答案,以便将集合属性更改为“list”而不是“int/float”。这个答案让人明白了,在这里self.value = [] 正是我所需要的! - benjaminmgross

14

我认为递归字典的最简实现是这样的。只有叶子节点可以包含值。

# Define recursive dictionary
from collections import defaultdict
tree = lambda: defaultdict(tree)

使用方法:

# Create instance
mydict = tree()

mydict['a'] = 1
mydict['b']['a'] = 2
mydict['c']
mydict['d']['a']['b'] = 0

# Print
import prettyprint
prettyprint.pp(mydict)

输出:

{
  "a": 1, 
  "b": {
    "a": 1
  }, 
  "c": {},
  "d": {
    "a": {
      "b": 0
    }
  }
}

刚刚注意到我的帖子是#2的重复。抱歉。 - Bouke Versteegh
2
这可能是一个重复的问题,但我认为这个例子非常生动和有用,所以我会说你肯定添加了一些有用的内容。 - Mad Physicist
这是一个非常清晰的解释...谢谢。 - M__

11

我会用一个dict的子类来实现,该子类定义了__missing__方法:

>>> class NestedDict(dict):
...     def __missing__(self, key):
...             self[key] = NestedDict()
...             return self[key]
...
>>> table = NestedDict()
>>> table['A']['B1']['C1'] = 1
>>> table
{'A': {'B1': {'C1': 1}}}

使用defaultdict无法直接实现这一点,因为defaultdict在初始化时期望拥有工厂函数,但是在初始化时期,无法描述相同的defaultdict。上述结构完成了default dict的相同操作,但由于它是一个命名类(NestedDict),所以它可以在遇到缺失键时引用自身。还可以通过子类化defaultdict并覆盖__init__来实现。


这还不够。如果你尝试 table['A']['B1']['C1']['D2'] = 2,你会得到一个错误。节点必须能够保存值 子节点。 - Apalala
3
根据提供的示例输入,节点似乎只需要能够保存一个值或子节点,而不是同时保存这两者。因此,@Jason和我认为你提供的答案中的“value”属性是不必要的。 - martineau
@martinau 我的意见是,除非用树形结构解决,否则所有这些都会变得不稳定(容易出错)。语法和实现都不重要。这是否需要树形结构是一个问题吗?我的观点是,除非有充分的理由去做,否则不应该将设计强制成“漂亮”的语法。保持简单。 - Apalala
@Apalala 我知道这是老问题了,但我们如何实现一个既包含值又包含子节点的 defaultdict - Halcyon Abraham Ramirez
@HalcyonAbrahamRamirez,请看Apalala在同一个问题中的答案。 - Jason R. Coombs

6
这与上面的代码等效,但避免了lambda符号。也许更容易阅读?
def dict_factory():
   return defaultdict(dict_factory)

your_dict = dict_factory()

此外,根据评论,如果你想从现有的字典进行更新,你可以直接调用:
your_dict[0][1][2].update({"some_key":"some_value"})

为了向字典中添加值。

这个解决方案没有提供传递初始值的能力。我认为丹·奥伊金(Dan O'Huiginn)的解决方案(通过Dvd Avins的帖子)因此略微更好。 - Scott P.

4

Dan O'Huiginn在他的博客上发布了一个非常好的解决方案,2010年:

http://ohuiginn.net/mt/2010/07/nested_dictionaries_in_python.html

>>> class NestedDict(dict):
...     def __getitem__(self, key):
...         if key in self: return self.get(key)
...         return self.setdefault(key, NestedDict())


>>> eggs = NestedDict()
>>> eggs[1][2][3][4][5]
{}
>>> eggs
{1: {2: {3: {4: {5: {}}}}}}

1
当我想快速创建一个嵌套字典时,我觉得这种方法很好。如果我想要“重新启用” KeyError,使用 dict() 转换回标准字典很容易。 - JS.
return self.setdefault(key, NestedDict()) 就足够了,不需要 if。 - Scott P.

3
您可以使用一个递归的`defaultdict`实现此操作,具体请参考defaultdict
from collections import defaultdict

def tree():
    def the_tree():
        return defaultdict(the_tree)
    return the_tree()

重要的是要在闭包(“私有”局部函数范围)中保护默认工厂名称,这里是the_tree。避免使用一行代码的lambda版本,由于Python的后期绑定闭包,它存在错误,并改用def实现。
接受的答案使用lambda存在一个缺陷,即实例必须依赖于外部作用域中存在的nested_dict名称。如果由于某种原因无法解析工厂名称(例如,它被重新绑定或删除),则预先存在的实例也将变得微妙地损坏:
>>> nested_dict = lambda: defaultdict(nested_dict)
>>> nest = nested_dict()
>>> nest[0][1][2][3][4][6] = 7
>>> del nested_dict
>>> nest[8][9] = 10
# NameError: name 'nested_dict' is not defined

2

补充一下@Hugo的回答:
要设置最大深度:

l=lambda x:defaultdict(lambda:l(x-1)) if x>0 else defaultdict(dict)
arr = l(2)

2
一种略有不同的可能性是允许常规字典初始化:
from collections import defaultdict

def superdict(arg=()):
    update = lambda obj, arg: obj.update(arg) or obj
    return update(defaultdict(superdict), arg)

例子:

>>> d = {"a":1}
>>> sd = superdict(d)
>>> sd["b"]["c"] = 2

1
您可以使用一个嵌套字典NestedDict
from ndicts.ndicts import NestedDict

nd = NestedDict()
nd[0, 1, 2, 3, 4, 5] = 6

结果以字典形式呈现:

>>> nd.to_dict()
{0: {1: {2: {3: {4: {5: 6}}}}}}

安装ndicts

pip install ndicts

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接