web-dev-qa-db-fra.com

Plusieurs niveaux de 'collection.defaultdict' dans Python

Grâce à de grands spécialistes de SO, j'ai découvert les possibilités offertes par collections.defaultdict, notamment en lisibilité et rapidité. Je les ai mis à profit avec succès.

Maintenant, j'aimerais implémenter trois niveaux de dictionnaires, les deux premiers étant defaultdict et le plus bas étant int. Je ne trouve pas le moyen approprié de le faire. Voici ma tentative:

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]

Maintenant, cela fonctionne, mais le comportement suivant, qui correspond au comportement souhaité, ne fonctionne pas:

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

Je suppose que j'aurais dû déclarer quelque part que le deuxième niveau defaultdict est de type int, mais je n'ai pas trouvé où ni comment le faire.

La raison pour laquelle j'utilise defaultdict en premier lieu est d'éviter d'avoir à initialiser le dictionnaire pour chaque nouvelle clé.

Une suggestion plus élégante?

Merci pythoneers!

159
Morlock

Utilisation:

d = defaultdict(lambda: defaultdict(int))

Cela créera une nouvelle defaultdict(int) chaque fois qu'une nouvelle clé sera utilisée dans d.

303
interjay

Une autre façon de créer un defaultdict imbriqué et picklable consiste à utiliser un objet partiel au lieu d'un lambda:

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

Cela fonctionnera car la classe defaultdict est globalement accessible au niveau du module:

"Vous ne pouvez décaper un objet partiel que si la fonction [ou, dans ce cas, la classe] qu’il recouvre est globalement accessible ... sous son __name__ (dans son __module__)" - fonctions partielles enveloppées dans le pickling

30
Nathaniel Gentile

Regardez la réponse de nosklo ici pour une solution plus générale.

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

Essai:

a = AutoVivification()

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

print a

Sortie:

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

Comme demandé par @ rschwieb pour D['key'] += 1, Nous pouvons développer previous en surchargeant l'ajout en définissant la méthode __add__, Pour que cela se comporte davantage comme un collections.Counter()

D'abord, __missing__ Sera appelé pour créer une nouvelle valeur vide, qui sera passée dans __add__. Nous testons la valeur, en comptant sur les valeurs vides pour être False.

Voir émulation de types numériques pour plus d'informations sur le remplacement.

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

Exemples:

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

Plutôt que de vérifier l'argument comme un nombre (très non python, amirite!), Nous pourrions simplement fournir une valeur par défaut de 0, puis tenter l'opération:

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
4
spazm

En retard pour le parti, mais pour une profondeur arbitraire, je me suis retrouvé à faire quelque chose comme ceci:

from collections import defaultdict

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

Le truc ici consiste essentiellement à faire de l’instance DeepDict une fabrique valide pour la construction des valeurs manquantes. Maintenant nous pouvons faire des choses comme

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
3
Rad Haring
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]: ''

Pas d'erreur encore. Peu importe le nombre de niveaux imbriqués. pop pas d'erreur aussi

dd = DefaultDict ({"1": 333333})

1
ACE Fly