Plusieurs niveaux de 'collection.defaultdict' en Python


176

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

Je voudrais maintenant implémenter trois niveaux de dictionnaires, les deux premiers étant defaultdictet le plus bas int. Je ne trouve pas la manière appropriée de faire cela. 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 ce qui suit, qui est le comportement souhaité, ne fonctionne pas:

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

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

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

Une suggestion plus élégante?

Merci pythoneers!

Réponses:


341

Utilisation:

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

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


2
Le seul problème est que cela ne se décapera pas, ce qui signifie qu'il multiprocessingest mécontent de les envoyer dans les deux sens.
Noah

19
@Noah: Il sera pickle si vous utilisez une fonction nommée au niveau du module au lieu d'un lambda.
entre

4
@ScienceFriction Avez-vous besoin d'aide pour quelque chose de spécifique? Lorsqu'on d[new_key]y accède, il appellera le lambda qui créera un nouveau defaultdict(int). Et quand d[existing_key][new_key2]on y accède, un nouveau intsera créé.
entre

11
C'est génial. Il semble que je renouvelle quotidiennement mes vœux matrimoniaux à Python.
mVChr

3
Vous recherchez plus de détails sur l'utilisation de cette méthode avec multiprocessinget en quoi consiste une fonction nommée au niveau du module? Cette question fait suite.
Cecilia

32

Une autre façon de rendre un defaultdict imbriqué pickleable 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 pas décaper un objet partiel à moins que la fonction [ou dans ce cas, la classe] qu'il enveloppe soit globalement accessible ... sous son __name__ (dans son __module__)" - Décapage des fonctions partielles enveloppées


12

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

Production:

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

Merci pour le lien @ miles82 (et la modification, @voyager). Dans quelle mesure cette approche est-elle pythonesque et sûre?
Morlock

2
Malheureusement, cette solution ne préserve pas la partie la plus pratique de defaultdict, qui est le pouvoir d'écrire quelque chose comme D ['key'] + = 1 sans se soucier de l'existence de la clé. C'est la principale fonctionnalité pour laquelle j'utilise defaultdict ... mais je peux imaginer que l'approfondissement dynamique des dictionnaires est également très pratique.
rschwieb

2
@rschwieb vous pouvez ajouter le pouvoir d'écrire + = 1 en ajoutant la méthode add .
spazm

5

Conformément à la demande de @ rschwieb pour D['key'] += 1, nous pouvons développer le précédent en remplaçant l'addition en définissant__add__ méthode, pour que cela se comporte plus comme uncollections.Counter()

Le premier __missing__sera appelé pour créer une nouvelle valeur vide, qui sera transmise __add__. Nous testons la valeur en comptant sur des valeurs vides 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 que l'argument est un nombre (très non-python, amirite!), Nous pourrions simplement fournir une valeur par défaut 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

devraient-ils déclencher NotImplemented plutôt que ValueError?
spazm

5

Tard à la fête, mais pour une profondeur arbitraire, je me suis juste retrouvé à faire quelque chose comme ça:

from collections import defaultdict

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

L'astuce ici est essentiellement de faire de l' DeepDictinstance elle-même 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

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

Aucune erreur à nouveau. Peu importe le nombre de niveaux imbriqués. pop pas d'erreur aussi

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.