¿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.d['primary']['secondary'].setdefault('tertiary', []).append('element')
- ?? Gracias por el cumplido, pero permítanme ser sincero, nunca lo uso__missing__
, siempre lo usosetdefault
. Probablemente debería actualizar mi conclusión / introducción ...The bad lookup will remain in the dictionary.
considerar esta solución? Muy apreciado. Thxsetdefault
cuando anidara más de dos niveles de profundidad. Parece que ninguna estructura en Python puede ofrecer una verdadera vivificación como se describe. Tuve que conformarme con dos métodos de declaración, uno paraget_nested
y otro para losset_nested
cuales aceptaba una referencia para dict y una lista de atributos anidados.Pruebas:
Salida:
fuente
pickle
es terrible entre las versiones de Python. Evite usarlo para almacenar los datos que desea conservar. Úselo solo para cachés y cosas que puede volcar y regenerar a voluntad. No como un método de almacenamiento o serialización a largo plazo.sqlite
base de datos para almacenarlos.Solo porque no he visto uno tan pequeño, aquí hay un dict que se anida como quieras, sin problemas:
fuente
yodict = lambda: defaultdict(yodict)
.dict
, por lo que para ser completamente equivalentes tendríamos quex = Vdict(a=1, b=2)
trabajar.dict
no era un requisito establecido por el OP, que solo solicitó la "mejor manera" de implementarlos, y además, no debería / no debería importa tanto en Python de todos modos.Puede crear un archivo YAML y leerlo usando PyYaml .
Paso 1: Cree un archivo YAML, "empleo.yml":
Paso 2: léelo en Python
y ahora
my_shnazzy_dictionary
tiene todos tus valores. Si necesita hacer esto sobre la marcha, puede crear el YAML como una cadena y alimentarloyaml.safe_load(...)
.fuente
Como tiene un diseño de esquema en estrella, es posible que desee estructurarlo más como una tabla relacional y menos como un diccionario.
Ese tipo de cosas puede recorrer un largo camino para crear un diseño similar a un almacén de datos sin los gastos generales de SQL.
fuente
Si el número de niveles de anidación es pequeño, lo uso
collections.defaultdict
para esto:Utilizando
defaultdict
como esto evita un montón de desordenadosetdefault()
,get()
etc.fuente
Esta es una función que devuelve un diccionario anidado de profundidad arbitraria:
Úselo así:
Repetir todo con algo como esto:
Esto imprime:
Es posible que eventualmente desee hacerlo para que no se puedan agregar nuevos elementos al dict. Es fácil convertir de forma recursiva todos estos
defaultdict
s a normalesdict
.fuente
Me parece
setdefault
bastante útil; Comprueba si hay una clave presente y la agrega si no:setdefault
siempre devuelve la clave relevante, por lo que en realidad está actualizando los valores de 'd
' en su lugar.Cuando se trata de iterar, estoy seguro de que podría escribir un generador con la suficiente facilidad si aún no existe uno en Python:
fuente
Como otros han sugerido, una base de datos relacional podría ser más útil para usted. Puede usar una base de datos sqlite3 en memoria como estructura de datos para crear tablas y luego consultarlas.
Este es solo un ejemplo simple. Puede definir tablas separadas para estados, condados y títulos de trabajo.
fuente
collections.defaultdict
se puede subclasificar para hacer un dict anidado. Luego agregue cualquier método de iteración útil a esa clase.fuente
En cuanto a "molestos bloques try / catch":
rendimientos
Puede usar esto para convertir su formato de diccionario plano a formato estructurado:
fuente
Puedes usar Addict: https://github.com/mewwts/addict
fuente
defaultdict()
¡es tu amigo!Para un diccionario bidimensional puedes hacer:
Para más dimensiones puedes:
fuente
Para iterar fácilmente sobre su diccionario anidado, ¿por qué no simplemente escribir un generador simple?
Entonces, si tiene su diccionario anidado compilado, iterar sobre él se vuelve simple:
Obviamente, su generador puede producir cualquier formato de datos que sea útil para usted.
¿Por qué estás usando try catch blocks para leer el árbol? Es bastante fácil (y probablemente más seguro) consultar si existe una clave en un dict antes de intentar recuperarla. Una función que usa cláusulas de protección podría verse así:
O, un método quizás algo detallado, es usar el método get:
Pero para una forma un poco más sucinta, es posible que desee considerar el uso de collections.defaultdict , que forma parte de la biblioteca estándar desde python 2.5.
Aquí estoy haciendo suposiciones sobre el significado de su estructura de datos, pero debería ser fácil de ajustar para lo que realmente quiere hacer.
fuente
Me gusta la idea de envolver esto en una clase e implementar
__getitem__
y__setitem__
tal que implementaron un lenguaje de consulta simple:Si quisieras ponerte elegante, también podrías implementar algo como:
pero sobre todo creo que tal cosa sería muy divertida de implementar: D
fuente
A menos que su conjunto de datos se mantenga bastante pequeño, puede considerar usar una base de datos relacional. Hará exactamente lo que desee: facilitar la adición de recuentos, la selección de subconjuntos de recuentos e incluso los recuentos agregados por estado, condado, ocupación o cualquier combinación de estos.
fuente
Ejemplo:
Editar: ahora devuelve diccionarios cuando se consulta con comodines (
None
), y valores individuales de lo contrario.fuente
Tengo una cosa similar en marcha. Tengo muchos casos en los que hago:
Pero yendo a muchos niveles de profundidad. Es el ".get (item, {})" que es la clave, ya que creará otro diccionario si aún no lo hay. Mientras tanto, he estado pensando en formas de lidiar con esto mejor. En este momento, hay muchos
Entonces, en cambio, hice:
Lo que tiene el mismo efecto si haces:
¿Mejor? Creo que sí.
fuente
Puede usar la recursión en lambdas y defaultdict, no es necesario definir nombres:
Aquí hay un ejemplo:
fuente
Solía usar esta función. Es seguro, rápido y fácil de mantener.
Ejemplo:
fuente