Python: defaultdict de defaultdict?

323

Existe-t-il un moyen d'avoir un defaultdict(defaultdict(int))afin de faire fonctionner le code suivant?

for x in stuff:
    d[x.a][x.b] += x.c_int

ddoit être construit de manière ad hoc, en fonction des éléments x.aet x.b.

Je pourrais utiliser:

for x in stuff:
    d[x.a,x.b] += x.c_int

mais je ne pourrais pas utiliser:

d.keys()
d[x.a].keys()
Jonathan
la source
6
Voir la question similaire Quelle est la meilleure façon d'implémenter des dictionnaires imbriqués en Python? . Il y a aussi des informations éventuellement utiles dans l'article de Wikipedia sur l' Autovivification .
martineau

Réponses:

571

Oui comme ça:

defaultdict(lambda: defaultdict(int))

L'argument d'un defaultdict(dans ce cas est lambda: defaultdict(int)) sera appelé lorsque vous essayez d'accéder à une clé qui n'existe pas. La valeur de retour de celle-ci sera définie comme la nouvelle valeur de cette clé, ce qui signifie dans notre cas que la valeur de d[Key_doesnt_exist]sera defaultdict(int).

Si vous essayez d'accéder à une clé de ce dernier defaultdict, c'est- d[Key_doesnt_exist][Key_doesnt_exist]à- dire qu'elle retournera 0, qui est la valeur de retour de l'argument du dernier defaultdict, c'est-à-dire int().

mouad
la source
7
ça marche super! pourriez-vous expliquer le rationnel derrière cette syntaxe?
Jonathan
37
@Jonathan: Oui bien sûr, l'argument d'un defaultdict(dans ce cas est lambda : defaultdict(int)) sera appelé lorsque vous essayez d'accéder à une clé qui n'existe pas et sa valeur de retour sera définie comme la nouvelle valeur de cette clé, ce qui signifie dans dans notre cas, la valeur de d[Key_dont_exist]sera defaultdict(int), et si vous essayez d'accéder à une clé de ce dernier défaut, c'est- d[Key_dont_exist][Key_dont_exist]à- dire qu'elle renverra 0, qui est la valeur de retour de l'argument du dernier, defaultdictc'est-à- dire que j'espère que cela vous aura int()été utile.
mouad
25
L'argument to defaultdictdevrait être une fonction. defaultdict(int)est un dictionnaire, tandis que sa lambda: defaultdict(int)fonction renvoie un dictionnaire.
has2k1
27
@ has2k1 C'est incorrect. L'argument de defaultdict doit être appelable. Un lambda est un appelable.
Niels Bom
2
@RickyLevi, si vous voulez que cela fonctionne, vous pouvez simplement dire: defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
darophi
51

Le paramètre du constructeur defaultdict est la fonction qui sera appelée pour construire de nouveaux éléments. Alors utilisons un lambda!

>>> from collections import defaultdict
>>> d = defaultdict(lambda : defaultdict(int))
>>> print d[0]
defaultdict(<type 'int'>, {})
>>> print d[0]["x"]
0

Depuis Python 2.7, il existe une solution encore meilleure en utilisant Counter :

>>> from collections import Counter
>>> c = Counter()
>>> c["goodbye"]+=1
>>> c["and thank you"]=42
>>> c["for the fish"]-=5
>>> c
Counter({'and thank you': 42, 'goodbye': 1, 'for the fish': -5})

Quelques bonus

>>> c.most_common()[:2]
[('and thank you', 42), ('goodbye', 1)]

Pour plus d'informations, consultez PyMOTW - Collections - Types de données de conteneur et documentation Python - collections

yanjost
la source
5
Juste pour compléter le cercle ici, vous voudriez utiliser d = defaultdict(lambda : Counter())plutôt que d = defaultdict(lambda : defaultdict(int))de traiter spécifiquement le problème tel qu'il a été posé à l'origine.
gumption
3
@gumption vous pouvez simplement utiliser d = defaultdict(Counter())pas besoin d'un lambda dans ce cas
Deb
3
@Deb vous avez une légère erreur - supprimez les parenthèses internes afin de passer un appelable au lieu d'un Counterobjet. Soit:d = defaultdict(Counter)
Dillon Davis
29

Je le trouve un peu plus élégant à utiliser partial:

import functools
dd_int = functools.partial(defaultdict, int)
defaultdict(dd_int)

Bien sûr, c'est la même chose qu'une lambda.

Katriel
la source
1
Le partiel est également meilleur que lambda ici car il peut être appliqué de manière récursive :) voir ma réponse ci-dessous pour une méthode d'usine par défaut imbriquée par défaut.
Campi
@Campi vous n'avez pas besoin de partiels pour les applications récursives, AFAICT
Clément
10

Pour référence, il est possible d'implémenter une defaultdictméthode d'usine imbriquée générique via:

from collections import defaultdict
from functools import partial
from itertools import repeat


def nested_defaultdict(default_factory, depth=1):
    result = partial(defaultdict, default_factory)
    for _ in repeat(None, depth - 1):
        result = partial(defaultdict, result)
    return result()

La profondeur définit le nombre de dictionnaires imbriqués avant que le type défini dans ne default_factorysoit utilisé. Par exemple:

my_dict = nested_defaultdict(list, 3)
my_dict['a']['b']['c'].append('e')
Campi
la source
Pouvez-vous donner un exemple d'utilisation? Ne fonctionne pas comme je m'y attendais. ndd = nested_defaultdict(dict) .... ndd['a']['b']['c']['d'] = 'e'lancersKeyError: 'b'
David Marx
Hé David, vous devez définir la profondeur de votre dictionnaire, dans votre exemple 3 (comme vous avez également défini default_factory comme dictionnaire. Nested_defaultdict (dict, 3) fonctionnera pour vous.
Campi
C'était super utile, merci! Une chose que j'ai remarquée est que cela crée un default_dict at depth=0, qui peut ne pas toujours être souhaité si la profondeur est inconnue au moment de l'appel. Facilement réparable en ajoutant une ligne if not depth: return default_factory(), en haut de la fonction, bien qu'il existe probablement une solution plus élégante.
Brendan
9

Les réponses précédentes ont porté sur la façon de créer un niveau à deux niveaux ou n defaultdict. Dans certains cas, vous en voulez un infini:

def ddict():
    return defaultdict(ddict)

Usage:

>>> d = ddict()
>>> d[1]['a'][True] = 0.5
>>> d[1]['b'] = 3
>>> import pprint; pprint.pprint(d)
defaultdict(<function ddict at 0x7fcac68bf048>,
            {1: defaultdict(<function ddict at 0x7fcac68bf048>,
                            {'a': defaultdict(<function ddict at 0x7fcac68bf048>,
                                              {True: 0.5}),
                             'b': 3})})
Clément
la source
1
J'aime cela. C'est diaboliquement simple, mais incroyablement utile. Merci!
rosstex
6

D'autres ont répondu correctement à votre question sur la façon de faire fonctionner les éléments suivants:

for x in stuff:
    d[x.a][x.b] += x.c_int

Une alternative serait d'utiliser des tuples pour les clés:

d = defaultdict(int)
for x in stuff:
    d[x.a,x.b] += x.c_int
    # ^^^^^^^ tuple key

La bonne chose à propos de cette approche est qu'elle est simple et peut être facilement développée. Si vous avez besoin d'une cartographie à trois niveaux, utilisez simplement un tuple à trois éléments pour la clé.

Steven Rumbalski
la source
4
Cette solution signifie qu'il n'est pas simple d'obtenir tout d [xa], car vous devez examiner chaque clé pour voir si elle a xa comme premier élément du tuple.
Matthew Schinckel
5
Si vous vouliez imbriquer 3 niveaux en profondeur, définissez-le simplement comme 3 niveaux: d = defaultdict (lambda: defaultdict (lambda: defaultdict (int)))
Matthew Schinckel