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

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().


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


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.


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

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

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.


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
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.