¿Cuál es la mejor manera de implementar diccionarios anidados en Python?
Esta es una mala idea, no lo hagas. En su lugar, use un diccionario regular y use dict.setdefault
donde sea apropiado, de modo que cuando faltan claves bajo el uso normal, obtenga lo esperado KeyError
. Si insiste en obtener este comportamiento, aquí le mostramos cómo pegarse un tiro en el pie:
Implemente __missing__
en una dict
subclase para establecer y devolver una nueva instancia.
Este enfoque ha estado disponible (y documentado) desde Python 2.5, y (particularmente valioso para mí) se imprime como un dict normal , en lugar de la impresión fea de un veredicto predeterminado autovivido:
class Vividict(dict):
def __missing__(self, key):
value = self[key] = type(self)() # retain local pointer to value
return value # faster to return than dict lookup
(La nota self[key]
está en el lado izquierdo de la asignación, por lo que no hay recursividad aquí).
y digamos que tienes algunos datos:
data = {('new jersey', 'mercer county', 'plumbers'): 3,
('new jersey', 'mercer county', 'programmers'): 81,
('new jersey', 'middlesex county', 'programmers'): 81,
('new jersey', 'middlesex county', 'salesmen'): 62,
('new york', 'queens county', 'plumbers'): 9,
('new york', 'queens county', 'salesmen'): 36}
Aquí está nuestro código de uso:
vividict = Vividict()
for (state, county, occupation), number in data.items():
vividict[state][county][occupation] = number
Y ahora:
>>> import pprint
>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36}}}
Crítica
Una crítica a este tipo de contenedor es que si el usuario escribe mal una clave, nuestro código podría fallar en silencio:
>>> vividict['new york']['queens counyt']
{}
Y además ahora tendríamos un condado mal escrito en nuestros datos:
>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36},
'queens counyt': {}}}
Explicación:
Solo proporcionamos otra instancia anidada de nuestra clase Vividict
cada vez que se accede a una clave pero falta. (Devolver la asignación de valor es útil porque nos evita además llamar al getter en el dict, y desafortunadamente, no podemos devolverlo ya que se está configurando).
Tenga en cuenta que estas son la misma semántica que la respuesta más votada pero en la mitad de las líneas de código: la implementación de nosklo:
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
Demostración de uso
A continuación se muestra solo un ejemplo de cómo este dict podría usarse fácilmente para crear una estructura de dict anidada sobre la marcha. Esto puede crear rápidamente una estructura de árbol jerárquica tan profundamente como desee.
import pprint
class Vividict(dict):
def __missing__(self, key):
value = self[key] = type(self)()
return value
d = Vividict()
d['foo']['bar']
d['foo']['baz']
d['fizz']['buzz']
d['primary']['secondary']['tertiary']['quaternary']
pprint.pprint(d)
Qué salidas:
{'fizz': {'buzz': {}},
'foo': {'bar': {}, 'baz': {}},
'primary': {'secondary': {'tertiary': {'quaternary': {}}}}}
Y como muestra la última línea, se imprime muy bien y con el fin de realizar una inspección manual. Pero si desea inspeccionar visualmente sus datos, la implementación __missing__
para establecer una nueva instancia de su clase en la clave y devolverla es una solución mucho mejor.
Otras alternativas, por contraste:
dict.setdefault
Aunque el autor de la pregunta piensa que esto no está limpio, me parece preferible a Vividict
mí mismo.
d = {} # or dict()
for (state, county, occupation), number in data.items():
d.setdefault(state, {}).setdefault(county, {})[occupation] = number
y ahora:
>>> pprint.pprint(d, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36}}}
Un error ortográfico fallaría ruidosamente y no saturaría nuestros datos con mala información:
>>> d['new york']['queens counyt']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'queens counyt'
Además, creo que setdefault funciona muy bien cuando se usa en bucles y no sabes lo que vas a obtener para las claves, pero el uso repetitivo se vuelve bastante pesado, y no creo que nadie quiera seguir con lo siguiente:
d = dict()
d.setdefault('foo', {}).setdefault('bar', {})
d.setdefault('foo', {}).setdefault('baz', {})
d.setdefault('fizz', {}).setdefault('buzz', {})
d.setdefault('primary', {}).setdefault('secondary', {}).setdefault('tertiary', {}).setdefault('quaternary', {})
Otra crítica es que setdefault requiere una nueva instancia, se use o no. Sin embargo, Python (o al menos CPython) es bastante inteligente sobre el manejo de nuevas instancias no utilizadas y sin referencia, por ejemplo, reutiliza la ubicación en la memoria:
>>> id({}), id({}), id({})
(523575344, 523575344, 523575344)
Un defaultdict vivificado automáticamente
Esta es una implementación de aspecto ordenado, y el uso en un script en el que no está inspeccionando los datos sería tan útil como la implementación __missing__
:
from collections import defaultdict
def vivdict():
return defaultdict(vivdict)
Pero si necesita inspeccionar sus datos, los resultados de un fallo predeterminado auto-vivificado poblado con datos de la misma manera se ven así:
>>> d = vivdict(); d['foo']['bar']; d['foo']['baz']; d['fizz']['buzz']; d['primary']['secondary']['tertiary']['quaternary']; import pprint;
>>> pprint.pprint(d)
defaultdict(<function vivdict at 0x17B01870>, {'foo': defaultdict(<function vivdict
at 0x17B01870>, {'baz': defaultdict(<function vivdict at 0x17B01870>, {}), 'bar':
defaultdict(<function vivdict at 0x17B01870>, {})}), 'primary': defaultdict(<function
vivdict at 0x17B01870>, {'secondary': defaultdict(<function vivdict at 0x17B01870>,
{'tertiary': defaultdict(<function vivdict at 0x17B01870>, {'quaternary': defaultdict(
<function vivdict at 0x17B01870>, {})})})}), 'fizz': defaultdict(<function vivdict at
0x17B01870>, {'buzz': defaultdict(<function vivdict at 0x17B01870>, {})})})
Este resultado es bastante poco elegante, y los resultados son bastante ilegibles. La solución típicamente dada es convertir recursivamente a un dict para inspección manual. Esta solución no trivial se deja como un ejercicio para el lector.
Actuación
Finalmente, veamos el rendimiento. Estoy restando los costos de la instanciación.
>>> import timeit
>>> min(timeit.repeat(lambda: {}.setdefault('foo', {}))) - min(timeit.repeat(lambda: {}))
0.13612580299377441
>>> min(timeit.repeat(lambda: vivdict()['foo'])) - min(timeit.repeat(lambda: vivdict()))
0.2936999797821045
>>> min(timeit.repeat(lambda: Vividict()['foo'])) - min(timeit.repeat(lambda: Vividict()))
0.5354437828063965
>>> min(timeit.repeat(lambda: AutoVivification()['foo'])) - min(timeit.repeat(lambda: AutoVivification()))
2.138362169265747
Basado en el rendimiento, dict.setdefault
funciona mejor. Lo recomiendo encarecidamente para el código de producción, en los casos en que le interese la velocidad de ejecución.
Si necesita esto para uso interactivo (tal vez en un portátil IPython), entonces el rendimiento realmente no importa, en cuyo caso, iría con Vividict para la legibilidad de la salida. En comparación con el objeto AutoVivification (que usa en __getitem__
lugar de __missing__
, que fue hecho para este propósito), es muy superior.
Conclusión
Implementar __missing__
en una subclase dict
para establecer y devolver una nueva instancia es un poco más difícil que las alternativas, pero tiene los beneficios de
- instanciación fácil
- población de datos fácil
- fácil visualización de datos
y debido a que es menos complicado y más eficaz que modificar __getitem__
, debería preferirse ese método.
Sin embargo, tiene inconvenientes:
- Las malas búsquedas fallarán en silencio.
- La mala búsqueda permanecerá en el diccionario.
Por lo tanto, personalmente prefiero setdefault
las otras soluciones, y tengo en cada situación donde he necesitado este tipo de comportamiento.
Vividict
? Por ejemplo,3
ylist
para un dict de dict de dict de listas que podrían ser pobladasd['primary']['secondary']['tertiary'].append(element)
. Podría definir 3 clases diferentes para cada profundidad, pero me encantaría encontrar una solución más limpia.