Python: defaultdict of defaultdict?


323

C'è un modo per avere un defaultdict(defaultdict(int))per far funzionare il seguente codice?

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

ddeve essere costruita ad-hoc, seconda x.ae x.belementi.

Potrei usare:

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

ma poi non sarei in grado di usare:

d.keys()
d[x.a].keys()

6
Vedi domanda simile Qual è il modo migliore per implementare dizionari nidificati in Python? . Ci sono anche alcune informazioni forse utili nell'articolo di Wikipedia sull'autovivificazione .
martineau,

Risposte:


571

Sì come questo:

defaultdict(lambda: defaultdict(int))

L'argomento di un defaultdict(in questo caso è lambda: defaultdict(int)) verrà chiamato quando si tenta di accedere a una chiave che non esiste. Il valore di ritorno verrà impostato come nuovo valore di questa chiave, il che significa che nel nostro caso il valore di d[Key_doesnt_exist]sarà defaultdict(int).

Se si tenta di accedere a una chiave da questo ultimo defaultdict cioè d[Key_doesnt_exist][Key_doesnt_exist]restituirà 0, che è il valore restituito dall'argomento dell'ultimo defaultdict ie int().


7
funziona benissimo! potresti spiegare il razionale dietro questa sintassi?
Jonathan,

37
@Jonathan: Sì certo, l'argomento di un defaultdict(in questo caso è lambda : defaultdict(int)) verrà chiamato quando si tenta di accedere a una chiave che non esiste e il valore restituito verrà impostato come nuovo valore di questa chiave che significa in nel nostro caso il valore di d[Key_dont_exist]sarà defaultdict(int), e se provi ad accedere a una chiave da questo ultimo defaultdict cioè d[Key_dont_exist][Key_dont_exist]restituirà 0 che è il valore di ritorno dell'argomento dell'ultimo defaultdictie int(), spero che questo sia stato utile.
mouad,

25
L'argomento defaultdictdovrebbe essere una funzione. defaultdict(int)è un dizionario, mentre lambda: defaultdict(int)è una funzione che restituisce un dizionario.
has2k1,

27
@ has2k1 È errato. L'argomento di defaultdict deve essere richiamabile. Una lambda è un callable.
Niels Bom,

2
@RickyLevi, se vuoi che funzioni, puoi solo dire: defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
darophi,

51

Il parametro per il costruttore defaultdict è la funzione che verrà chiamata per la creazione di nuovi elementi. Quindi usiamo un lambda!

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

Da Python 2.7, c'è una soluzione ancora migliore usando 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})

Alcune funzioni bonus

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

Per ulteriori informazioni, consultare PyMOTW - Collezioni - Tipi di dati contenitore e Documentazione Python - Collezioni


5
Solo per completare il cerchio qui, dovresti usare d = defaultdict(lambda : Counter())piuttosto che d = defaultdict(lambda : defaultdict(int))affrontare specificamente il problema come originariamente posto.
gosing

3
@gumption d = defaultdict(Counter())in questo caso non puoi semplicemente usare nessuna lambda
Deb

3
@Deb hai un leggero errore- rimuovi le parentesi interne in modo da passare un callable invece di un Counteroggetto. Cioè:d = defaultdict(Counter)
Dillon Davis l'

29

Lo trovo leggermente più elegante da usare partial:

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

Certo, questo è lo stesso di una lambda.


1
Parziale è anche meglio di lambda qui perché può essere applicato in modo ricorsivo :) vedere la mia risposta di seguito per un metodo di fabbrica defaultdict nidificato generico.
Campi

@Campi non è necessario parziale per le applicazioni ricorsive, AFAICT
Clément

10

Per riferimento, è possibile implementare un defaultdictmetodo factory nidificato generico tramite:

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 profondità definisce il numero di dizionario nidificato prima dell'utilizzo del tipo definito in default_factory. Per esempio:

my_dict = nested_defaultdict(list, 3)
my_dict['a']['b']['c'].append('e')

Puoi fare un esempio di utilizzo? Non funziona nel modo previsto. ndd = nested_defaultdict(dict) .... ndd['a']['b']['c']['d'] = 'e'lanciaKeyError: 'b'
David Marx,

Ehi David, devi definire la profondità del tuo dizionario, nel tuo esempio 3 (dato che hai definito default_factory anche come dizionario. Nested_defaultdict (dict, 3) funzionerà per te.
Campi

Questo è stato molto utile, grazie! Una cosa che ho notato è che questo crea un default_dict su depth=0, che potrebbe non essere sempre desiderato se la profondità non è nota al momento della chiamata. Facilmente risolvibile aggiungendo una linea if not depth: return default_factory(), nella parte superiore della funzione, anche se probabilmente c'è una soluzione più elegante.
Brendan

9

Le risposte precedenti hanno indicato come creare due livelli o livelli n defaultdict. In alcuni casi ne vuoi uno infinito:

def ddict():
    return defaultdict(ddict)

Uso:

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

1
Amo questo. È diabolicamente semplice, ma incredibilmente utile. Grazie!
rosstex

6

Altri hanno risposto correttamente alla tua domanda su come far funzionare quanto segue:

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

Un'alternativa sarebbe usare le tuple per le chiavi:

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

La cosa bella di questo approccio è che è semplice e può essere facilmente ampliato. Se hai bisogno di una mappatura profonda tre livelli, usa semplicemente una tupla a tre elementi per la chiave.


4
Questa soluzione significa che non è semplice ottenere tutto d [xa], poiché è necessario introspettare ogni chiave per vedere se ha xa come primo elemento della tupla.
Matthew Schinckel

5
Se desideri annidare 3 livelli in profondità, definiscilo come 3 livelli: d = defaultdict (lambda: defaultdict (lambda: defaultdict (int)))
Matthew Schinckel,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.