嵌套的defaultdict of defaultdict

216
有没有办法让defaultdict也成为defaultdict的默认值?(即无限级递归defaultdict?)
我希望能够这样做:
x = defaultdict(...stuff...)
x[0][1][0]
{}

所以,我可以做x = defaultdict(defaultdict),但那只是第二层。
x[0]
{}
x[0][0]
KeyError: 0

有一些配方可以做到这一点。但是只使用普通的defaultdict参数就能简单地做到吗?
请注意,这是在询问如何创建一个无限级递归的defaultdict,因此与“Python:defaultdict of defaultdict?”这个问题是不同的,那个问题是关于如何创建一个两级defaultdict。
我可能最终会使用“bunch”模式,但当我意识到我不知道如何做到这一点时,我对此产生了兴趣。

可能是Python:defaultdict of defaultdict?的重复问题。 - malioboro
2
并不是真的...我在问题中添加了信息来说明原因。虽然那是一个有用的问题。 - Corley Brigman
12个回答

318

这里的其他答案告诉你如何创建一个包含“无限多个” defaultdict 的字典,但它们未解决我认为可能是你最初需求的问题,那就是简单地拥有一个两层深度的 defaultdict。

你可能一直在寻找:

defaultdict(lambda: defaultdict(dict))

您可能更喜欢这个构造的原因是:

  • 它比递归解决方案更明确,因此读者更容易理解。
  • 这使得defaultdict的“叶子”可以是除字典之外的其他类型,例如:defaultdict(lambda:defaultdict(list))defaultdict(lambda:defaultdict(set))

5
defaultdict(lambda: defaultdict(list)) 的正确形式是什么? - Yuvaraj Loganathan
6
这个问题被标记为可能是另一个问题的重复,但这不是我的原始问题。我知道如何创建一个两级defaultdict;我不知道的是如何使它递归。实际上,这个答案类似于 https://dev59.com/om445IYBdhLWcg3wH2rH - Corley Brigman
1
Lambda方法的一个缺点是它生成的对象无法被pickle,但是您可以通过在pickle之前将其转换为常规的dict(result)来解决这个问题。 - CpILL
这似乎仅适用于递归级别大于1的情况。例如,nested_dict['foo']['bar'].append('baz')是有效的,但是nested_dict['foo'].append('bar')会失败,因为defaultdict类没有append属性。 - Addison Klinke
@CpILL:对于固定级别的情况,可以很容易地通过使用defaultdict(defaultdict(dict).copy)来解决。无需转换回dict,因为模板defaultdict的绑定方法是可序列化的,而lambda则不是。 - undefined
显示剩余2条评论

257

对于任意层数:

def rec_dd():
    return defaultdict(rec_dd)

>>> x = rec_dd()
>>> x['a']['b']['c']['d']
defaultdict(<function rec_dd at 0x7f0dcef81500>, {})
>>> print json.dumps(x)
{"a": {"b": {"c": {"d": {}}}}}
当然,您也可以使用lambda来完成这个操作,但我发现lambda难以阅读。无论如何,具体做法如下:
rec_dd = lambda: defaultdict(rec_dd)

4
确实是个完美的例子,谢谢。你能否把它扩展到一种情况,即将数据从JSON加载到defaultdict中的defaultdict - David Belohrad
6
注意:如果您正在尝试在进行数据序列化时使用此代码,则无法使用 lambda 函数。 - Viacheslav Kondratiuk
4
我们如何定义字典底部最后一个值的类型? - Diamond
1
这个程序非常“神奇”,让人有点害怕,但它按照预期工作! - Dre
1
知道这已经过时了,但希望有人回复。你如何正确地向rec_dd函数添加类型提示? - Robert Alexander
显示剩余2条评论

84

有一个聪明的技巧可以做到这一点:

tree = lambda: defaultdict(tree)

然后,您可以使用x = tree()创建您的x


2
回到这个问题,我认为这是我使用最多的一段代码 :) - Corley Brigman

23

与 BrenBarn 的解决方案类似,但不包含变量 tree 的名称两次,因此即使更改了变量字典,它也可以工作:

类似于 BrenBarn 的解决方案,但不重复使用变量 `tree` 的名称,这样即使变量字典发生变化,代码也可正常工作。
tree = (lambda f: f(f))(lambda a: (lambda: defaultdict(a(a))))

然后您可以使用x = tree()来创建每个新的x


对于def版本,我们可以使用函数闭包作用域来保护数据结构免受tree名称重新绑定导致现有实例停止工作的缺陷。代码如下:

from collections import defaultdict

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

4
我需要考虑一下这个问题(它有点复杂)。但我认为你的观点是,如果执行x = tree(),但稍后有人将tree=None,那么前者仍然有效,而后者则无效? - Corley Brigman

23

我还建议更多采用面向对象编程(OOP)的实现方式,该方式支持无限嵌套并能够正确格式化repr

class NestedDefaultDict(defaultdict):
    def __init__(self, *args, **kwargs):
        super(NestedDefaultDict, self).__init__(NestedDefaultDict, *args, **kwargs)

    def __repr__(self):
        return repr(dict(self))

用法:

my_dict = NestedDefaultDict()
my_dict['a']['b'] = 1
my_dict['a']['c']['d'] = 2
my_dict['b']

print(my_dict)  # {'a': {'b': 1, 'c': {'d': 2}}, 'b': {}}

2
太好了!我添加了*args**kwargs的传递,使它能像defaultdict一样运行,即创建一个带有关键字参数的字典。这对于将NestedDefaultDict传递到json.load非常有用。 - Ciprian Tomoiagă
尝试使用my_dict = NestedDefaultDict(list)会返回一个TypeError错误 - *args是否打算以这种方式允许叶子类型的定义呢? - Addison Klinke
@AddisonKlinke 不,在这个实现中不是这样的。default_factory 参数已经被 NestedDefaultDict 类型占用。没有简单的方法可以检查当前节点是否是叶子节点,而不需要构建更复杂的类。但是,您可以编写类似于 my_dict ['a'] ['b'] [0] 的内容来模拟具有列表类型的节点。 - Stanislav Tsepa
很遗憾,这个答案似乎不支持多进程。 - Chris Coffee
我喜欢这个!然而,它不支持增量或列表添加: my_dict ['a'] ['b'] + = 1 ...会失败。是否有任何方法可以使其支持其余的字典操作? - RandallShanePhD

1
我基于Andrew在这里的answer。如果你想从json或现有的字典中加载数据到嵌套的defaultdict中,请参考以下示例:
def nested_defaultdict(existing=None, **kwargs):
    if existing is None:
        existing = {}
    if not isinstance(existing, dict):
        return existing
    existing = {key: nested_defaultdict(val) for key, val in existing.items()}
    return defaultdict(nested_defaultdict, existing, **kwargs)

https://gist.github.com/nucklehead/2d29628bb49115f3c30e78c071207775


1
这是一个针对任意嵌套深度的任意基础defaultdict的函数。
(从无法pickle defaultdict跨贴)
def wrap_defaultdict(instance, times=1):
    """Wrap an instance an arbitrary number of `times` to create nested defaultdict.
    
    Parameters
    ----------
    instance - list, dict, int, collections.Counter
    times - the number of nested keys above `instance`; if `times=3` dd[one][two][three] = instance
    
    Notes
    -----
    using `x.copy` allows pickling (loading to ipyparallel cluster or pkldump)
        - thanks https://dev59.com/sWQn5IYBdhLWcg3w9K_3
    """
    from collections import defaultdict

    def _dd(x):
        return defaultdict(x.copy)

    dd = defaultdict(instance)
    for i in range(times-1):
        dd = _dd(dd)

    return dd

0
这是一个类似于@Stanislav答案的解决方案,它可以与多进程一起使用,并允许终止嵌套:
from collections import defaultdict
from functools import partial

class NestedDD(defaultdict):
    def __init__(self, n, *args, **kwargs):
        self.n = n
        factory = partial(build_nested_dd, n=n - 1) if n > 1 else int
        super().__init__(factory, *args, **kwargs)

    def __repr__(self):
        return repr(dict(self))

def build_nested_dd(n):
    return NestedDD(n)

0

根据Chris W的回答,为了解决类型注释的问题,您可以将其作为工厂函数来定义详细的类型。例如,这是我在研究这个问题时得出的最终解决方案:

def frequency_map_factory() -> dict[str, dict[str, int]]:
    """
    Provides a recorder of: per X:str, frequency of Y:str occurrences.
    """
    return defaultdict(lambda: defaultdict(int))

0
这是一个递归函数,用于将递归默认字典转换为普通字典。
def defdict_to_dict(defdict, finaldict):
    # pass in an empty dict for finaldict
    for k, v in defdict.items():
        if isinstance(v, defaultdict):
            # new level created and that is the new value
            finaldict[k] = defdict_to_dict(v, {})
        else:
            finaldict[k] = v
    return finaldict

defdict_to_dict(my_rec_default_dict, {})

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