Python中的“ collection.defaultdict”多个级别


174

感谢SO方面的一些杰出人士,我发现了的可能性collections.defaultdict,尤其是在可读性和速度方面。我让他们成功使用。

现在,我想实现三个级别的字典,两个最大的字典是defaultdict,最低的是int。我找不到执行此操作的适当方法。这是我的尝试:

from collections import defaultdict
d = defaultdict(defaultdict)
a = [("key1", {"a1":22, "a2":33}),
     ("key2", {"a1":32, "a2":55}),
     ("key3", {"a1":43, "a2":44})]
for i in a:
    d[i[0]] = i[1]

现在这可以工作,但是以下是所需的行为,但无效:

d["key4"]["a1"] + 1

我怀疑我应该在某个地方声明第二个级别defaultdict是type int,但是我没有找到在哪里或怎么做。

defaultdict首先使用的原因是避免必须为每个新键初始化字典。

还有更优雅的建议吗?

谢谢pythoneers!

Answers:


338

用:

from collections import defaultdict
d = defaultdict(lambda: defaultdict(int))

defaultdict(int)只要在中访问新密钥,就会创建一个新密钥d


2
唯一的问题是它不会泡菜,这意味着multiprocessing对来回发送这些消息感到不满意。
挪亚2012年

19
@Noah:如果您使用命名模块级别的函数而不是lambda,它将使您不寒而栗。
interjay 2012年

4
@ScienceFriction您需要帮助的任何具体内容吗?当d[new_key]被访问时,它将调用lambda,它将创建一个new defaultdict(int)。并且当d[existing_key][new_key2]访问时,int将创建一个新的。
2013年

11
这太棒了。看来我每天都重申对Python的婚姻誓言。
mVChr 2014年

3
寻找有关使用此方法的更多细节multiprocessing以及什么是命名模块级函数?这个问题跟进。
Cecilia

32

使可腌制的嵌套defaultdict的另一种方法是使用部分对象而不是lambda:

from functools import partial
...
d = defaultdict(partial(defaultdict, int))

这将起作用,因为defaultdict类可在模块级别全局访问:

“除非对它包装的函数[或在这种情况下,类]可以在其__name__(在其__module__内)全局访问,否则您不能腌制部分对象” – 酸洗包装的部分函数


12

这里查看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

测试:

a = AutoVivification()

a[1][2][3] = 4
a[1][3][3] = 5
a[1][2]['test'] = 6

print a

输出:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}

感谢您提供链接@ miles82(以及编辑@voyager)。这种方法的Python风格和安全性如何?
莫洛克2010年

2
不幸的是,该解决方案没有保留defaultdict的最方便部分,即可以写D ['key'] + = 1之类的功能而不必担心密钥的存在。这是我将defaultdict用于...的主要功能,但是我可以想象动态加深词典也非常方便。
rschwieb 2014年

2
@rschwieb,您可以通过添加add方法来增加写+ = 1的能力。
spazm

5

按照@rschwieb的要求D['key'] += 1,我们可以通过定义方法覆盖加法来扩展前一个__add__方法,以使其表现得更像collections.Counter()

首先__missing__将被调用以创建一个新的空值,该值将传递到中__add__。我们测试该值,以空值为False

有关覆盖的更多信息,请参见模拟数字类型

from numbers import Number


class autovivify(dict):
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

    def __add__(self, x):
        """ override addition for numeric types when self is empty """
        if not self and isinstance(x, Number):
            return x
        raise ValueError

    def __sub__(self, x):
        if not self and isinstance(x, Number):
            return -1 * x
        raise ValueError

例子:

>>> import autovivify
>>> a = autovivify.autovivify()
>>> a
{}
>>> a[2]
{}
>>> a
{2: {}}
>>> a[4] += 1
>>> a[5][3][2] -= 1
>>> a
{2: {}, 4: 1, 5: {3: {2: -1}}}

我们可以只提供默认的0值,然后尝试操作:

class av2(dict):
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

    def __add__(self, x):
        """ override addition when self is empty """
        if not self:
            return 0 + x
        raise ValueError

    def __sub__(self, x):
        """ override subtraction when self is empty """
        if not self:
            return 0 - x
        raise ValueError

这些应该引发NotImplemented而不是ValueError吗?
spazm 2014年

5

晚会晚了,但是对于任意深度,我只是发现自己在做这样的事情:

from collections import defaultdict

class DeepDict(defaultdict):
    def __call__(self):
        return DeepDict(self.default_factory)

这里的窍门基本上是使DeepDict实例本身成为构造缺失值的有效工厂。现在我们可以做类似的事情

dd = DeepDict(DeepDict(list))
dd[1][2].extend([3,4])
sum(dd[1][2])  # 7

ddd = DeepDict(DeepDict(DeepDict(list)))
ddd[1][2][3].extend([4,5])
sum(ddd[1][2][3])  # 9

1
def _sub_getitem(self, k):
    try:
        # sub.__class__.__bases__[0]
        real_val = self.__class__.mro()[-2].__getitem__(self, k)
        val = '' if real_val is None else real_val
    except Exception:
        val = ''
        real_val = None
    # isinstance(Avoid,dict)也是true,会一直递归死
    if type(val) in (dict, list, str, tuple):
        val = type('Avoid', (type(val),), {'__getitem__': _sub_getitem, 'pop': _sub_pop})(val)
        # 重新赋值当前字典键为返回值,当对其赋值时可回溯
        if all([real_val is not None, isinstance(self, (dict, list)), type(k) is not slice]):
            self[k] = val
    return val


def _sub_pop(self, k=-1):
    try:
        val = self.__class__.mro()[-2].pop(self, k)
        val = '' if val is None else val
    except Exception:
        val = ''
    if type(val) in (dict, list, str, tuple):
        val = type('Avoid', (type(val),), {'__getitem__': _sub_getitem, 'pop': _sub_pop})(val)
    return val


class DefaultDict(dict):
    def __getitem__(self, k):
        return _sub_getitem(self, k)

    def pop(self, k):
        return _sub_pop(self, k)

In[8]: d=DefaultDict()
In[9]: d['a']['b']['c']['d']
Out[9]: ''
In[10]: d['a']="ggggggg"
In[11]: d['a']
Out[11]: 'ggggggg'
In[12]: d['a']['pp']
Out[12]: ''

再没有错误。无论嵌套多少级。弹出也没有错误

dd = DefaultDict({“ 1”:333333})

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.