Multiple levels of 'collection.defaultdict'

2019-01-04 06:32发布

Thanks to some great folks on SO, I discovered the possibilities offered by collections.defaultdict, notably in readability and speed. I have put them to use with success.

Now I would like to implement three levels of dictionaries, the two top ones being defaultdict and the lowest one being int. I don't find the appropriate way to do this. Here is my attempt:

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]

Now this works, but the following, which is the desired behavior, doesn't:

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

I suspect that I should have declared somewhere that the second level defaultdict is of type int, but I didn't find where or how to do so.

The reason I am using defaultdict in the first place is to avoid having to initialize the dictionary for each new key.

Any more elegant suggestion?

Thanks pythoneers!

6条回答
唯我独甜
2楼-- · 2019-01-04 07:15

Use:

d = defaultdict(lambda: defaultdict(int))

This will create a new defaultdict(int) whenever a new key is accessed in d.

查看更多
我欲成王,谁敢阻挡
3楼-- · 2019-01-04 07:16

Look at nosklo's answer here for a more general solution.

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

Testing:

a = AutoVivification()

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

print a

Output:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}
查看更多
孤傲高冷的网名
4楼-- · 2019-01-04 07:22

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

    def pop(self, k):
        return DefaultDict.sub_pop(self, k)

    @staticmethod
    def sub_pop(sub, k):
        try:
            return dict.pop(sub, k)
        except Exception:
            return ""

    @staticmethod
    def sub_getitem(sub, k):
        err_flag = False
        try:
            # sub.__class__.__bases__[0]
            val = sub.__class__.mro()[-2].__getitem__(sub, k)
            if val is None:
                val = ''
        except Exception:
            val = ''
            err_flag = True
        # do not use isinstance here, because isinstance(Avoid,dict) == true
        if type(val) in (dict, list, str, tuple):
            cl = type('Avoid', (type(val),), {'__getitem__': DefaultDict.sub_getitem, 'pop': DefaultDict.sub_pop})
            val = cl(val)
            if not err_flag and type(sub) is [dict, list] and type(k) is not slice:
                print(k)
                sub[k] = val
        return val

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]: ''

No errors again. 
No matter how many levels  nested.
pop  no error also

dd=DefaultDict({"1":333333})
查看更多
forever°为你锁心
5楼-- · 2019-01-04 07:28

As per @rschwieb's request for D['key'] += 1, we can expand on previous by overriding addition by defining __add__ method, to make this behave more like a collections.Counter()

First __missing__ will be called to create a new empty value, which will be passed into __add__. We test the value, counting on empty values to be False.

See emulating numeric types for more information on overriding.

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

Examples:

>>> 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}}}

Rather than checking argument is a Number (very non-python, amirite!) we could just provide a default 0 value and then attempt the operation:

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
查看更多
forever°为你锁心
6楼-- · 2019-01-04 07:29

Late to the party, but for arbitrary depth I just found myself doing something like this:

from collections import defaultdict

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

The trick here is basically to make the DeepDict instance itself a valid factory for constructing missing values. Now we can do things like

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

ffffd = DeepDict(DeepDict(DeepDict(list)))
ffffd[1][2][3].extend([4,5])
sum(ffffd[1][2][3])  # 9
查看更多
Evening l夕情丶
7楼-- · 2019-01-04 07:30

Another way to make a pickleable, nested defaultdict is to use a partial object instead of a lambda:

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

This will work because the defaultdict class is globally accessible at the module level:

"You can't pickle a partial object unless the function [or in this case, class] it wraps is globally accessible ... under its __name__ (within its __module__)" -- Pickling wrapped partial functions

查看更多
登录 后发表回答