Múltiples niveles de 'collection.defaultdict' en Python

176

Gracias a algunas personas excelentes en SO, descubrí las posibilidades que ofrece collections.defaultdict, especialmente en legibilidad y velocidad. Los he puesto en uso con éxito.

Ahora me gustaría implementar tres niveles de diccionarios, siendo los dos superiores defaultdicty el más bajo int. No encuentro la forma adecuada de hacer esto. Aquí está 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 esto funciona, pero lo siguiente, que es el comportamiento deseado, no funciona:

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

Sospecho que debería haber declarado en alguna parte que el segundo nivel defaultdictes de tipo int, pero no encontré dónde ni cómo hacerlo.

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

¿Alguna sugerencia más elegante?

Gracias pitoneros!

Morlock
fuente

Respuestas:

341

Utilizar:

from collections import defaultdict
d = defaultdict(lambda: defaultdict(int))

Esto creará una nueva defaultdict(int)cada vez que se acceda a una nueva clave d.

interjay
fuente
2
El único problema es que no se encurtirá, lo que significa que no multiprocessingestá contento con enviarlos de un lado a otro.
Noah
19
@Noah: Se encurtirá si usa una función de nivel de módulo con nombre en lugar de una lambda.
Interjay
44
@ScienceFriction ¿Algo específico con lo que necesita ayuda? Cuando d[new_key]se accede, llamará a la lambda que creará una nueva defaultdict(int). Y cuando d[existing_key][new_key2]se accede, se intcreará una nueva .
Interjay
11
Esto es asombroso Parece que renuevo mis votos matrimoniales a Python diariamente.
mVChr
3
¿Busca más detalles sobre el uso de este método multiprocessingy qué es una función de nivel de módulo con nombre? Esta pregunta sigue.
Cecilia
32

Otra forma de hacer un veredicto predeterminado anidado y encurtido es usar un objeto parcial en lugar de una lambda:

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

Esto funcionará porque la clase defaultdict es accesible globalmente a nivel de módulo:

"No puede encurtir un objeto parcial a menos que la función [o en este caso, la clase] que ajusta sea accesible globalmente ... bajo su __nombre__ (dentro de su __módulo__)" - Decapado de funciones parciales envueltas

Nathaniel Gentile
fuente
12

Mira la respuesta de nosklo aquí 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

Pruebas:

a = AutoVivification()

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

print a

Salida:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}
millas82
fuente
Gracias por el enlace @ miles82 (y la edición, @voyager). ¿Cuán pitonesco y seguro es este enfoque?
Morlock
2
Desafortunadamente, esta solución no conserva la parte más útil de defaultdict, que es el poder de escribir algo como D ['clave'] + = 1 sin preocuparse por la existencia de la clave. Esa es la característica principal para la que utilizo defaultdict ... pero me imagino que los diccionarios que se profundizan dinámicamente también son bastante útiles.
rschwieb
2
@rschwieb puede agregar el poder de escribir + = 1 agregando el método add .
spazm
5

De acuerdo con la solicitud de @ rschwieb para D['key'] += 1, podemos ampliar la anterior anulando la suma definiendo el __add__método, para que esto se comporte más como uncollections.Counter()

Primero __missing__se llamará para crear un nuevo valor vacío, que se pasará a __add__. Probamos el valor, contando con valores vacíos para ser False.

Consulte emulación de tipos numéricos 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 verificar el argumento es un Número (¡muy poco python, amirite!) Podríamos proporcionar un valor 0 predeterminado 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
Spazm
fuente
¿Deberían estos generar NotImplemented en lugar de ValueError?
spazm
5

Tarde a la fiesta, pero por profundidad arbitraria me encontré haciendo algo como esto:

from collections import defaultdict

class DeepDict(defaultdict):
    def __call__(self):
        return DeepDict(self.default_factory)

El truco aquí es básicamente hacer de la DeepDictinstancia en sí una fábrica válida para construir valores perdidos. Ahora podemos hacer cosas como

dd = DeepDict(DeepDict(list))
dd[1][2].extend([3,4])
sum(dd[1][2])  # 7

ddd = DeepDict(DeepDict(DeepDict(list)))
ddd[1][2][3].extend([4,5])
sum(ddd[1][2][3])  # 9
Rad Haring
fuente
1
def _sub_getitem(self, k):
    try:
        # sub.__class__.__bases__[0]
        real_val = self.__class__.mro()[-2].__getitem__(self, k)
        val = '' if real_val is None else real_val
    except Exception:
        val = ''
        real_val = None
    # isinstance(Avoid,dict)也是true,会一直递归死
    if type(val) in (dict, list, str, tuple):
        val = type('Avoid', (type(val),), {'__getitem__': _sub_getitem, 'pop': _sub_pop})(val)
        # 重新赋值当前字典键为返回值,当对其赋值时可回溯
        if all([real_val is not None, isinstance(self, (dict, list)), type(k) is not slice]):
            self[k] = val
    return val


def _sub_pop(self, k=-1):
    try:
        val = self.__class__.mro()[-2].pop(self, k)
        val = '' if val is None else val
    except Exception:
        val = ''
    if type(val) in (dict, list, str, tuple):
        val = type('Avoid', (type(val),), {'__getitem__': _sub_getitem, 'pop': _sub_pop})(val)
    return val


class DefaultDict(dict):
    def __getitem__(self, k):
        return _sub_getitem(self, k)

    def pop(self, k):
        return _sub_pop(self, k)

In[8]: d=DefaultDict()
In[9]: d['a']['b']['c']['d']
Out[9]: ''
In[10]: d['a']="ggggggg"
In[11]: d['a']
Out[11]: 'ggggggg'
In[12]: d['a']['pp']
Out[12]: ''

No hay errores de nuevo. No importa cuántos niveles anidados. pop sin error también

dd = DefaultDict ({"1": 333333})

ACE Fly
fuente