Python: defaultdict de defaultdict?

323

¿Hay alguna manera de tener un defaultdict(defaultdict(int))para que el siguiente código funcione?

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

dnecesita ser construido ad-hoc, dependiendo de x.ay x.belementos.

Podría usar:

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

pero entonces no podría usar:

d.keys()
d[x.a].keys()
Jonathan
fuente
66
Ver pregunta similar ¿ Cuál es la mejor manera de implementar diccionarios anidados en Python? . También hay información posiblemente útil en el artículo de Wikipedia sobre Autovivificación .
Martineau

Respuestas:

571

Sí, así:

defaultdict(lambda: defaultdict(int))

Se llamará al argumento de a defaultdict(en este caso lambda: defaultdict(int)) cuando intente acceder a una clave que no existe. El valor de retorno se establecerá como el nuevo valor de esta clave, lo que significa que en nuestro caso el valor de d[Key_doesnt_exist]será defaultdict(int).

Si intenta acceder a una clave desde este último defaultdict, es decir d[Key_doesnt_exist][Key_doesnt_exist], devolverá 0, que es el valor de retorno del argumento del último defaultdict, es decir int().

mouad
fuente
77
funciona muy bien! ¿Podría explicar lo racional detrás de esta sintaxis?
Jonathan
37
@ Jonathan: Sí, claro, se invocará el argumento de a defaultdict(en este caso lambda : defaultdict(int)) cuando intente acceder a una clave que no existe y el valor de retorno se establecerá como el nuevo valor de esta clave que significa en nuestro caso d[Key_dont_exist]será el valor de defaultdict(int), y si intenta acceder a una clave desde este último defaultdict, es decir d[Key_dont_exist][Key_dont_exist], devolverá 0, que es el valor de retorno del argumento del último , defaultdictes decir int(), espero que esto haya sido útil.
mouad
25
El argumento de defaultdictdebería ser una función. defaultdict(int)es un diccionario, mientras que lambda: defaultdict(int)es una función que devuelve un diccionario.
has2k1
27
@ has2k1 Eso es incorrecto. El argumento de defaultdict debe ser invocable. Una lambda es invocable.
Niels Bom
2
@RickyLevi, si quieres que funcione, puedes decir: defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
darophi
51

El parámetro para el constructor defaultdict es la función que se llamará para construir nuevos elementos. ¡Entonces usemos una lambda!

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

Desde Python 2.7, hay una solución aún mejor con 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})

Algunas características adicionales

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

Para obtener más información, consulte PyMOTW - Colecciones - Tipos de datos de contenedor y Documentación de Python - colecciones

Yanjost
fuente
55
Solo para completar el círculo aquí, querrá usar en d = defaultdict(lambda : Counter())lugar de d = defaultdict(lambda : defaultdict(int))abordar específicamente el problema como se planteó originalmente.
Gumption
3
@gumption, simplemente d = defaultdict(Counter())no puede usar una lambda en este caso
Deb
3
@Deb tiene un pequeño error: elimine los paréntesis internos para pasar un invocable en lugar de un Counterobjeto. Es decir:d = defaultdict(Counter)
Dillon Davis
29

Me parece un poco más elegante de usar partial:

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

Por supuesto, esto es lo mismo que una lambda.

Katriel
fuente
1
Parcial también es mejor que lambda aquí porque se puede aplicar de forma recursiva :) vea mi respuesta a continuación para obtener un método de fábrica genérico por defecto anidado.
Campi
@Campi no necesita parcial para aplicaciones recursivas, AFAICT
Clément
10

Como referencia, es posible implementar un defaultdictmétodo de fábrica anidado genérico a través de:

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 profundidad define el número de diccionarios anidados antes de default_factoryusar el tipo definido en . Por ejemplo:

my_dict = nested_defaultdict(list, 3)
my_dict['a']['b']['c'].append('e')
Campi
fuente
¿Puedes dar un ejemplo de uso? No funciona de la manera que esperaba. ndd = nested_defaultdict(dict) .... ndd['a']['b']['c']['d'] = 'e'tirosKeyError: 'b'
David Marx
Hola David, debes definir la profundidad de tu diccionario, en tu ejemplo 3 (como definiste default_factory para que también sea un diccionario. Nested_defaultdict (dict, 3) funcionará para ti.
Campi
Esto fue súper útil, gracias! Una cosa que noté es que esto crea un default_dict en depth=0, que no siempre se desea si la profundidad es desconocida al momento de la llamada. Fácilmente reparable agregando una línea if not depth: return default_factory(), en la parte superior de la función, aunque probablemente haya una solución más elegante.
Brendan
9

Las respuestas anteriores han abordado cómo hacer dos niveles o n niveles defaultdict. En algunos casos, quieres 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})})
Clemente
fuente
1
Me encanta esto. Es endiabladamente simple, pero increíblemente útil. ¡Gracias!
rosstex
6

Otros han respondido correctamente su pregunta sobre cómo hacer que funcione lo siguiente:

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

Una alternativa sería usar tuplas para claves:

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

Lo bueno de este enfoque es que es simple y se puede ampliar fácilmente. Si necesita un mapeo de tres niveles de profundidad, simplemente use una tupla de tres elementos para la clave.

Steven Rumbalski
fuente
44
Esta solución significa que no es simple obtener todo d [xa], ya que necesita introspectar cada clave para ver si tiene xa como primer elemento de la tupla.
Matthew Schinckel
55
Si desea anidar 3 niveles de profundidad, simplemente defínalo como 3 niveles: d = defaultdict (lambda: defaultdict (lambda: defaultdict (int)))
Matthew Schinckel