Defaultdict anidado de defaultdict

129

¿Hay alguna manera de hacer que un defaultdict también sea el predeterminado para el defaultdict? (es decir, ¿defaultdict recursivo de nivel infinito?)

Quiero poder hacer:

x = defaultdict(...stuff...)
x[0][1][0]
{}

Entonces, puedo hacerlo x = defaultdict(defaultdict), pero eso es solo un segundo nivel:

x[0]
{}
x[0][0]
KeyError: 0

Hay recetas que pueden hacer esto. Pero, ¿puede hacerse simplemente usando los argumentos normales de defaultdict?

Tenga en cuenta que esto es preguntar cómo hacer un defaultdict recursivo de nivel infinito, por lo que es distinto a Python: defaultdict de defaultdict? , que era cómo hacer un defaultdict de dos niveles.

Probablemente termine usando el patrón de grupo , pero cuando me di cuenta de que no sabía cómo hacer esto, me interesó.

Corley Brigman
fuente
Posible duplicado de Python: defaultdict de defaultdict?
malioboro
2
En realidad no ... agregué información a la pregunta para indicar por qué. Aunque esa es una pregunta útil.
Corley Brigman el

Respuestas:

168

Para un número arbitrario de niveles:

def rec_dd():
    return defaultdict(rec_dd)

>>> x = rec_dd()
>>> x['a']['b']['c']['d']
defaultdict(<function rec_dd at 0x7f0dcef81500>, {})
>>> print json.dumps(x)
{"a": {"b": {"c": {"d": {}}}}}

Por supuesto, también puedes hacer esto con una lambda, pero creo que las lambdas son menos legibles. En cualquier caso, se vería así:

rec_dd = lambda: defaultdict(rec_dd)
Andrew Clark
fuente
1
De hecho, un ejemplo perfecto, gracias. ¿Podría extenderlo al caso de que los datos se cargan desde json en defaultdict de defaultdict?
David Belohrad
44
Una nota. Si está intentando usar este código mientras el decapado lambdano funcionará.
Viacheslav Kondratiuk
167

Las otras respuestas aquí le dicen cómo crear un defaultdictque contiene "infinitamente muchos"defaultdict , pero no abordan lo que creo que pudo haber sido su necesidad inicial, que era simplemente tener un defecto predeterminado de dos profundidades.

Es posible que haya estado buscando:

defaultdict(lambda: defaultdict(dict))

Las razones por las que podría preferir esta construcción son:

  • Es más explícito que la solución recursiva y, por lo tanto, probablemente más comprensible para el lector.
  • Esto permite que la "hoja" de la defaultdictsea ​​algo más que un diccionario, por ejemplo ,: defaultdict(lambda: defaultdict(list))odefaultdict(lambda: defaultdict(set))
Chris W.
fuente
3
defaultdict (lambda: defaultdict (list)) ¿La forma correcta?
Yuvaraj Loganathan
Vaya, sí, el lambdaformulario es correcto, porque defaultdict(something)devuelve un objeto similar a un diccionario, ¡pero defaultdictespera un invocable! ¡Gracias!
Chris W.
44
Esto se marcó como un posible duplicado de otra pregunta ... pero no era mi pregunta original. Sabía cómo crear un defaultdict de dos niveles; Lo que no sabía era cómo hacerlo recursivo. Esta respuesta es, de hecho, similar a stackoverflow.com/questions/5029934/…
Corley Brigman el
Una desventaja del enfoque lambda es que los objetos que genera no pueden conservarse en vinagre ... pero puede evitar esto lanzándolo a un lugar regular dict(result)antes del encurtido
CpILL
54

Hay un ingenioso truco para hacer eso:

tree = lambda: defaultdict(tree)

Entonces puedes crear tu xcon x = tree().

BrenBarn
fuente
22

Similar a la solución de BrenBarn, pero no contiene el nombre de la variable treedos veces, por lo que funciona incluso después de los cambios en el diccionario de variables:

tree = (lambda f: f(f))(lambda a: (lambda: defaultdict(a(a))))

Entonces puedes crear cada nuevo xcon x = tree().


Para la defversión, podemos usar el alcance de cierre de funciones para proteger la estructura de datos de la falla donde las instancias existentes dejan de funcionar si el treenombre se recupera. Se parece a esto:

from collections import defaultdict

def tree():
    def the_tree():
        return defaultdict(the_tree)
    return the_tree()
pts
fuente
44
Tendré que pensar en esto (es un poco más complejo). pero creo que su punto es que si x = tree (), pero luego viene alguien y hace tree = None, ¿este todavía funcionaría, y eso no funcionaría?
Corley Brigman
11

También propondría más implementación de estilo OOP, que admite el anidamiento infinito y el formato correcto repr.

class NestedDefaultDict(defaultdict):
    def __init__(self, *args, **kwargs):
        super(NestedDefaultDict, self).__init__(NestedDefaultDict, *args, **kwargs)

    def __repr__(self):
        return repr(dict(self))

Uso:

my_dict = NestedDefaultDict()
my_dict['a']['b'] = 1
my_dict['a']['c']['d'] = 2
my_dict['b']

print(my_dict)  # {'a': {'b': 1, 'c': {'d': 2}}, 'b': {}}
Stanislav Tsepa
fuente
1
Ordenado ! Agregué el paso de *argsy **kwargsque le permite funcionar como el defaultdict, es decir, crear un dict con argumentos de palabras clave. Esto es útil para pasar NestedDefaultDictenjson.load
Ciprian Tomoiagă
0

Aquí hay una función recursiva para convertir un dict por defecto recursivo en un dict normal

def defdict_to_dict(defdict, finaldict):
    # pass in an empty dict for finaldict
    for k, v in defdict.items():
        if isinstance(v, defaultdict):
            # new level created and that is the new value
            finaldict[k] = defdict_to_dict(v, {})
        else:
            finaldict[k] = v
    return finaldict

defdict_to_dict(my_rec_default_dict, {})
Dr. XD
fuente
0

Basé esto de la respuesta de Andrew aquí. Si está buscando cargar datos de un json o un dict existente en el nesdict defaultdict, vea este ejemplo:

def nested_defaultdict(existing=None, **kwargs):
    if existing is None:
        existing = {}
    if not isinstance(existing, dict):
        return existing
    existing = {key: nested_defaultdict(val) for key, val in existing.items()}
    return defaultdict(nested_defaultdict, existing, **kwargs)

https://gist.github.com/nucklehead/2d29628bb49115f3c30e78c071207775

nucklehead
fuente