2010-04-08 14 views
116

Gracias a algunas personas geniales en SO, descubrí las posibilidades que ofrece collections.defaultdict, especialmente en legibilidad y velocidad. Los puse en uso con éxito.Múltiples niveles de 'collection.defaultdict' en Python

Ahora me gustaría implementar tres niveles de diccionarios, los dos superiores son defaultdict y el más bajo es int. No encuentro la manera apropiada de hacer esto. Aquí es mi intento:

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] 

Ahora bien, esto funciona, pero el siguiente, que es el comportamiento deseado, no:

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

sospecho que debería haber declarado en alguna parte que el segundo nivel defaultdict es del tipo int, pero no encontré dónde ni cómo hacerlo.

La razón por la que estoy usando defaultdict en primer lugar es para evitar tener que inicializar el diccionario para cada nueva clave.

¿Alguna sugerencia más elegante?

Gracias pythoneers!

Respuesta

244

Uso:

d = defaultdict(lambda: defaultdict(int)) 

Esto creará un nuevo defaultdict(int) cada vez que se accede a una nueva clave en d.

+0

El único problema es que no saldrá en vinagre, lo que significa 'multiprocesamiento' no está contento con enviarlos de un lado a otro. – Noah

+15

@Noah: saltará si utiliza una función denominada de nivel de módulo en lugar de una lambda. – interjay

+0

por supuesto, tonto yo. – Noah

10

Mire la respuesta de nosklo here para una solución más general.

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 

Testing:

a = AutoVivification() 

a[1][2][3] = 4 
a[1][3][3] = 5 
a[1][2]['test'] = 6 

print a 

Output:

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

Gracias por el enlace @ miles82 (y la edición, @voyager). ¿Cuán pitón y seguro es este enfoque? – Morlock

+0

Desafortunadamente, esta solución no conserva la parte más útil de defaultdict, que es el poder de escribir algo como D ['key'] + = 1 sin preocuparse por la existencia de la clave. Esa es la característica principal que uso defaultdict para ... pero puedo imaginar que los diccionarios de profundización dinámica también son bastante útiles. – rschwieb

+1

@rschwieb puede agregar la potencia para escribir + = 1 agregando el método __add__. – spazm

3

Según la petición de @ rschwieb para D['key'] += 1, podemos ampliar el previous anulando Además definiendo __add__ método, para que este se comporta más como un collections.Counter()

Primera __missing__ será llamado para crear un nuevo vacío valor, que se pasará a __add__. Probamos el valor, contando con valores vacíos que serán False.

Consulte emulating numeric types para obtener más información sobre la anulación.

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 

Ejemplos:

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

en lugar de comprobar argumento es un número (no muy pitón, amirite!) Que sólo podría proporcionar un valor por defecto 0 y luego intentar la operación:

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 
+0

¿deberían estos aumentar NotImplemented en lugar de ValueError? – spazm

13

Otra manera de hacer una pickleable, defaultdict anidada es utilizar un objeto parcial en lugar de una lambda:

from functools import partial 
... 
d = defaultdict(partial(defaultdict, int)) 

Este funcionará porque la clase defaultdict es accesible globalmente en el nivel de módulo:

"You can't pickle a partial object unless the function [or in this case, class] it wraps is globally accessible ... under its __name__ (within its __module__)" -- Pickling wrapped partial functions

Cuestiones relacionadas