¿Cuál es la mejor manera de implementar diccionarios anidados?


Tengo una estructura de datos que esencialmente equivale a un diccionario anidado. Digamos que se ve así:

{'new jersey': {'mercer county': {'plumbers': 3,
                                  'programmers': 81},
                'middlesex county': {'programmers': 81,
                                     'salesmen': 62}},
 'new york': {'queens county': {'plumbers': 9,
                                'salesmen': 36}}}

Ahora, mantener y crear esto es bastante doloroso; cada vez que tengo un nuevo estado/condado/profesión tengo que crear los diccionarios de la capa inferior a través de odiosos bloques try/catch. Además, tengo que crear iteradores anidados molestos si quiero repasar todos los valores.

También podría usar tuplas como claves, así:

{('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}

Esto hace que la iteración sobre los valores muy simple y natural, pero es más sintácticamente doloroso hacer cosas como agregaciones y mirando subconjuntos del diccionario (por ejemplo, si solo quiero ir estado por estado).

Básicamente, a veces quiero pensar en un diccionario anidado como un diccionario plano, y a veces quiero pensar en él de hecho como una jerarquía compleja. Podría envolver todo esto en una clase, pero parece que alguien podría haber hecho esto ya. Alternativamente, parece que podría haber algunos construcciones sintácticas muy elegantes para hacer esto.

¿Cómo podría hacerlo mejor?

Anexo: Soy consciente de setdefault() pero realmente no hace para la sintaxis limpia. Además, cada sub-diccionario que cree todavía necesita tener setdefault() configurado manualmente.

Author: martineau, 2009-03-11

20 answers

¿Cuál es la mejor manera de implementar diccionarios anidados en Python?

Implementa __missing__ en una subclase dict 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 bastante como un dict normal, en lugar de la impresión fea de un defaultdict autovivificado:{[37]]}

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

(Nota self[key] está en el lado izquierdo de la asignación, por lo que no hay recursión aquí.)

Y digamos que tienes algunos datos: {[37]]}

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 de este tipo de contenedor es que si el usuario escribe mal una clave, nuestro código podría fallar silenciosamente:

>>> 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 estamos proporcionando otra instancia anidada de nuestra clase Vividict siempre que una clave se accede pero falta. (Devolver la asignación de valor es útil porque nos evita llamar adicionalmente al getter en el dict, y desafortunadamente, no podemos devolverlo mientras se está configurando.)

Tenga en cuenta que estas son las mismas semánticas que la respuesta más votada, pero en la mitad de las líneas de implementación de code - 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 del uso

A continuación se muestra un ejemplo de cómo este dict podría usarse fácilmente para crear una estructura de dict anidada en el volar. Esto puede crear rápidamente una estructura jerárquica de árbol 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)

Que produce:

{'fizz': {'buzz': {}},
 'foo': {'bar': {}, 'baz': {}},
 'primary': {'secondary': {'tertiary': {'quaternary': {}}}}}

Y como muestra la última línea, se imprime muy bien y en orden para la inspección manual. Pero si desea inspeccionar visualmente sus datos, implementar __missing__ para establecer una nueva instancia de su clase en la clave y devolverla es una solución mucho mejor.

Otras alternativas, para contraste:

dict.setdefault

Aunque el preguntador piensa que esto no es limpio, me parece preferible a la Vividict yo 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 oneroso, y no creo que nadie quiera mantener 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 no referenciadas, por ejemplo, reutiliza la ubicación en la memoria:

>>> id({}), id({}), id({})
(523575344, 523575344, 523575344)

Un defaultdicto auto-vivificado

Esta es una implementación de aspecto limpio, y el uso en un script en el que no está inspeccionando los datos sería tan útil como implementar __missing__:

from collections import defaultdict

def vivdict():
    return defaultdict(vivdict)

Pero si necesita inspeccionar sus datos, los resultados de un defaultdict auto-vivificado poblado con datos de la misma manera se ve 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>, {})})})

Esta salida es bastante poco elegante, y los resultados son bastante ilegibles. La solución típicamente dada es convertir recursivamente de nuevo a un dict para la inspección manual. Esta solución no trivial se deja como un ejercicio para el lector.

Rendimiento

Finalmente, veamos el rendimiento. Estoy restando los costos de 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 los que se preocupa por la velocidad de ejecución.

Si necesita esto para uso interactivo (en un cuaderno IPython, tal vez), 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 de AutoVivificación (que utiliza __getitem__ en lugar de __missing__, que se hizo para este propósito) es muy superior.

Conclusión

Implementando __missing__ en un 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
  • easy data population
  • fácil visualización de datos

Y debido a que es menos complicado y más eficaz que modificar __getitem__, debe ser preferido a ese método.

Sin embargo, tiene inconvenientes:

  • Las búsquedas erróneas fallarán silenciosamente.
  • La mala búsqueda permanecer en el diccionario.

Así que personalmente prefiero setdefault a las otras soluciones, y tengo en cada situación donde he necesitado este tipo de comportamiento.

 143
Author: Aaron Hall,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-27 19:38:25
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}}}
 185
Author: nosklo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-16 21:53:36

Solo porque no he visto uno tan pequeño, aquí hay un dictado que se anida como quieras, no hay problema:

# yo dawg, i heard you liked dicts                                                                      
def yodict():
    return defaultdict(yodict)
 28
Author: paint can,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-07-06 21:41:28

Puede crear un archivo YAML y leerlo usando PyYAML.

Paso 1: Cree un archivo YAML, "empleo.yml":

new jersey:
  mercer county:
    pumbers: 3
    programmers: 81
  middlesex county:
    salesmen: 62
    programmers: 81
new york:
  queens county:
    plumbers: 9
    salesmen: 36

Paso 2: Léalo en Python

import yaml
file_handle = open("employment.yml")
my_shnazzy_dictionary = yaml.safe_load(file_handle)
file_handle.close()

Y ahora my_shnazzy_dictionary tiene todos tus valores. Si necesita hacer esto sobre la marcha, puede crear el YAML como una cadena y alimentarlo en yaml.safe_load(...).

 22
Author: Pete,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-07-06 21:42:20

Dado que tiene un diseño de esquema de estrella, es posible que desee estructurarlo más como una tabla relacional y menos como un diccionario.

import collections

class Jobs( object ):
    def __init__( self, state, county, title, count ):
        self.state= state
        self.count= county
        self.title= title
        self.count= count

facts = [
    Jobs( 'new jersey', 'mercer county', 'plumbers', 3 ),
    ...

def groupBy( facts, name ):
    total= collections.defaultdict( int )
    for f in facts:
        key= getattr( f, name )
        total[key] += f.count

Ese tipo de cosas pueden ayudar mucho a crear un diseño similar a un almacén de datos sin los gastos generales de SQL.

 17
Author: S.Lott,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-11 17:29:12

Si el número de niveles de anidamiento es pequeño, utilizo collections.defaultdict para esto:

from collections import defaultdict

def nested_dict_factory(): 
  return defaultdict(int)
def nested_dict_factory2(): 
  return defaultdict(nested_dict_factory)
db = defaultdict(nested_dict_factory2)

db['new jersey']['mercer county']['plumbers'] = 3
db['new jersey']['mercer county']['programmers'] = 81

Usar defaultdict de esta manera evita muchos problemas setdefault(), get(), etc.

 13
Author: user26294,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-07-06 21:45:25

Esta es una función que devuelve un diccionario anidado de profundidad arbitraria:

from collections import defaultdict
def make_dict():
    return defaultdict(make_dict)

Úsalo así:

d=defaultdict(make_dict)
d["food"]["meat"]="beef"
d["food"]["veggie"]="corn"
d["food"]["sweets"]="ice cream"
d["animal"]["pet"]["dog"]="collie"
d["animal"]["pet"]["cat"]="tabby"
d["animal"]["farm animal"]="chicken"

Itera a través de todo con algo como esto:

def iter_all(d,depth=1):
    for k,v in d.iteritems():
        print "-"*depth,k
        if type(v) is defaultdict:
            iter_all(v,depth+1)
        else:
            print "-"*(depth+1),v

iter_all(d)

Esto imprime:

- food
-- sweets
--- ice cream
-- meat
--- beef
-- veggie
--- corn
- animal
-- pet
--- dog
---- labrador
--- cat
---- tabby
-- farm animal
--- chicken

Es posible que eventualmente desee hacerlo para que no se puedan agregar nuevos elementos al dict. Es fácil convertir recursivamente todos estos defaultdict s a dict s normales.

def dictify(d):
    for k,v in d.iteritems():
        if isinstance(v,defaultdict):
            d[k] = dictify(v)
    return dict(d)
 9
Author: JnBrymn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-21 18:21:41

Encuentro setdefault bastante útil; Comprueba si una clave está presente y la agrega si no:

d = {}
d.setdefault('new jersey', {}).setdefault('mercer county', {})['plumbers'] = 3

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ías escribir un generador fácilmente si no existe ya en Python:

def iterateStates(d):
    # Let's count up the total number of "plumbers" / "dentists" / etc.
    # across all counties and states
    job_totals = {}

    # I guess this is the annoying nested stuff you were talking about?
    for (state, counties) in d.iteritems():
        for (county, jobs) in counties.iteritems():
            for (job, num) in jobs.iteritems():
                # If job isn't already in job_totals, default it to zero
                job_totals[job] = job_totals.get(job, 0) + num

    # Now return an iterator of (job, number) tuples
    return job_totals.iteritems()

# Display all jobs
for (job, num) in iterateStates(d):
    print "There are %d %s in total" % (job, num)
 7
Author: andygeers,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-07-06 21:47:22

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.

import sqlite3

c = sqlite3.Connection(':memory:')
c.execute('CREATE TABLE jobs (state, county, title, count)')

c.executemany('insert into jobs values (?, ?, ?, ?)', [
    ('New Jersey', 'Mercer County',    'Programmers', 81),
    ('New Jersey', 'Mercer County',    'Plumbers',     3),
    ('New Jersey', 'Middlesex County', 'Programmers', 81),
    ('New Jersey', 'Middlesex County', 'Salesmen',    62),
    ('New York',   'Queens County',    'Salesmen',    36),
    ('New York',   'Queens County',    'Plumbers',     9),
])

# some example queries
print list(c.execute('SELECT * FROM jobs WHERE county = "Queens County"'))
print list(c.execute('SELECT SUM(count) FROM jobs WHERE title = "Programmers"'))

Esto es solo un ejemplo simple. Puede definir tablas separadas para estados, condados y títulos de trabajo.

 6
Author: Roberto Bonvallet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-13 20:24:47

collections.defaultdict puede ser subclasificado para crear un dict anidado. Luego agregue cualquier método de iteración útil a esa clase.

>>> from collections import defaultdict
>>> class nesteddict(defaultdict):
    def __init__(self):
        defaultdict.__init__(self, nesteddict)
    def walk(self):
        for key, value in self.iteritems():
            if isinstance(value, nesteddict):
                for tup in value.walk():
                    yield (key,) + tup
            else:
                yield key, value


>>> nd = nesteddict()
>>> nd['new jersey']['mercer county']['plumbers'] = 3
>>> nd['new jersey']['mercer county']['programmers'] = 81
>>> nd['new jersey']['middlesex county']['programmers'] = 81
>>> nd['new jersey']['middlesex county']['salesmen'] = 62
>>> nd['new york']['queens county']['plumbers'] = 9
>>> nd['new york']['queens county']['salesmen'] = 36
>>> for tup in nd.walk():
    print tup


('new jersey', 'mercer county', 'programmers', 81)
('new jersey', 'mercer county', 'plumbers', 3)
('new jersey', 'middlesex county', 'programmers', 81)
('new jersey', 'middlesex county', 'salesmen', 62)
('new york', 'queens county', 'salesmen', 36)
('new york', 'queens county', 'plumbers', 9)
 5
Author: A. Coady,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-07-06 21:43:06

defaultdict() es tu amigo!

Para un diccionario bidimensional puedes hacer:

d = defaultdict(defaultdict)
d[1][2] = 3

Para más dimensiones puede:

d = defaultdict(lambda :defaultdict(defaultdict))
d[1][2][3] = 4
 5
Author: Paula,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-03 21:59:48

En cuanto a los "detestables bloques try/catch":

d = {}
d.setdefault('key',{}).setdefault('inner key',{})['inner inner key'] = 'value'
print d

Rinde

{'key': {'inner key': {'inner inner key': 'value'}}}

Puede usar esto para convertir de su formato de diccionario plano a formato estructurado:

fd = {('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}

for (k1,k2,k3), v in fd.iteritems():
    d.setdefault(k1, {}).setdefault(k2, {})[k3] = v
 4
Author: vartec,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-11 17:30:56

Para iterar fácilmente sobre su diccionario anidado, ¿por qué no escribir un generador simple?

def each_job(my_dict):
    for state, a in my_dict.items():
        for county, b in a.items():
            for job, value in b.items():
                yield {
                    'state'  : state,
                    'county' : county,
                    'job'    : job,
                    'value'  : value
                }

Entonces, si tienes tu diccionario anidado compilado, iterar sobre él se vuelve simple:

for r in each_job(my_dict):
    print "There are %d %s in %s, %s" % (r['value'], r['job'], r['county'], r['state'])

Obviamente su generador puede producir cualquier formato de datos que sea útil para usted.

¿Por qué estás usando bloques try catch para leer el árbol? Es bastante fácil (y probablemente más seguro) preguntar si existe una clave en un dictado antes de intentar recuperarla. Una función usando guard las cláusulas podrían tener este aspecto:

if not my_dict.has_key('new jersey'):
    return False

nj_dict = my_dict['new jersey']
...

O, un método quizás algo detallado, es usar el método get:

value = my_dict.get('new jersey', {}).get('middlesex county', {}).get('salesmen', 0)

Pero para una forma un poco más sucinta, es posible que desee mirar el uso de una colecciones.defaultdict, que es parte de la biblioteca estándar desde python 2.5.

import collections

def state_struct(): return collections.defaultdict(county_struct)
def county_struct(): return collections.defaultdict(job_struct)
def job_struct(): return 0

my_dict = collections.defaultdict(state_struct)

print my_dict['new jersey']['middlesex county']['salesmen']

Estoy haciendo suposiciones sobre el significado de su estructura de datos aquí, pero debería ser fácil de ajustar para lo que realmente quieres hacer.

 3
Author: SpoonMeiser,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-11 20:38:52

Puedes usar Addict: https://github.com/mewwts/addict

>>> from addict import Dict
>>> my_new_shiny_dict = Dict()
>>> my_new_shiny_dict.a.b.c.d.e = 2
>>> my_new_shiny_dict
{'a': {'b': {'c': {'d': {'e': 2}}}}}
 3
Author: JnBrymn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-21 18:50:09

Me gusta la idea de envolver esto en una clase e implementar __getitem__ y __setitem__ de manera que implementen un lenguaje de consulta simple:

>>> d['new jersey/mercer county/plumbers'] = 3
>>> d['new jersey/mercer county/programmers'] = 81
>>> d['new jersey/mercer county/programmers']
81
>>> d['new jersey/mercer country']
<view which implicitly adds 'new jersey/mercer county' to queries/mutations>

Si quieres ponerte elegante también puedes implementar algo como:

>>> d['*/*/programmers']
<view which would contain 'programmers' entries>

Pero sobre todo creo que tal cosa sería muy divertido de implementar: D

 2
Author: Aaron Maenpaa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-07-06 21:48:21

A menos que su conjunto de datos se mantenga bastante pequeño, es posible que desee considerar el uso de una base de datos relacional. Hará exactamente lo que desea: facilitar la adición de recuentos, la selección de subconjuntos de recuentos e incluso agregar recuentos por estado, condado, ocupación o cualquier combinación de estos.

 1
Author: allyourcode,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-11 20:30:20
class JobDb(object):
    def __init__(self):
        self.data = []
        self.all = set()
        self.free = []
        self.index1 = {}
        self.index2 = {}
        self.index3 = {}

    def _indices(self,(key1,key2,key3)):
        indices = self.all.copy()
        wild = False
        for index,key in ((self.index1,key1),(self.index2,key2),
                                             (self.index3,key3)):
            if key is not None:
                indices &= index.setdefault(key,set())
            else:
                wild = True
        return indices, wild

    def __getitem__(self,key):
        indices, wild = self._indices(key)
        if wild:
            return dict(self.data[i] for i in indices)
        else:
            values = [self.data[i][-1] for i in indices]
            if values:
                return values[0]

    def __setitem__(self,key,value):
        indices, wild = self._indices(key)
        if indices:
            for i in indices:
                self.data[i] = key,value
        elif wild:
            raise KeyError(k)
        else:
            if self.free:
                index = self.free.pop(0)
                self.data[index] = key,value
            else:
                index = len(self.data)
                self.data.append((key,value))
                self.all.add(index)
            self.index1.setdefault(key[0],set()).add(index)
            self.index2.setdefault(key[1],set()).add(index)
            self.index3.setdefault(key[2],set()).add(index)

    def __delitem__(self,key):
        indices,wild = self._indices(key)
        if not indices:
            raise KeyError
        self.index1[key[0]] -= indices
        self.index2[key[1]] -= indices
        self.index3[key[2]] -= indices
        self.all -= indices
        for i in indices:
            self.data[i] = None
        self.free.extend(indices)

    def __len__(self):
        return len(self.all)

    def __iter__(self):
        for key,value in self.data:
            yield key

Ejemplo:

>>> db = JobDb()
>>> db['new jersey', 'mercer county', 'plumbers'] = 3
>>> db['new jersey', 'mercer county', 'programmers'] = 81
>>> db['new jersey', 'middlesex county', 'programmers'] = 81
>>> db['new jersey', 'middlesex county', 'salesmen'] = 62
>>> db['new york', 'queens county', 'plumbers'] = 9
>>> db['new york', 'queens county', 'salesmen'] = 36

>>> db['new york', None, None]
{('new york', 'queens county', 'plumbers'): 9,
 ('new york', 'queens county', 'salesmen'): 36}

>>> db[None, None, 'plumbers']
{('new jersey', 'mercer county', 'plumbers'): 3,
 ('new york', 'queens county', 'plumbers'): 9}

>>> db['new jersey', 'mercer county', None]
{('new jersey', 'mercer county', 'plumbers'): 3,
 ('new jersey', 'mercer county', 'programmers'): 81}

>>> db['new jersey', 'middlesex county', 'programmers']
81

>>>

Edit: Ahora devuelve diccionarios cuando consulta con comodines (None), y valores simples de lo contrario.

 1
Author: Markus Jarderot,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-11 21:44:27

Puede usar recursión en lambdas y defaultdict, sin necesidad de definir nombres:

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

Aquí hay un ejemplo:

>>> a['new jersey']['mercer county']['plumbers']=3
>>> a['new jersey']['middlesex county']['programmers']=81
>>> a['new jersey']['mercer county']['programmers']=81
>>> a['new jersey']['middlesex county']['salesmen']=62
>>> a
defaultdict(<function __main__.<lambda>>,
        {'new jersey': defaultdict(<function __main__.<lambda>>,
                     {'mercer county': defaultdict(<function __main__.<lambda>>,
                                  {'plumbers': 3, 'programmers': 81}),
                      'middlesex county': defaultdict(<function __main__.<lambda>>,
                                  {'programmers': 81, 'salesmen': 62})})})
 1
Author: topkara,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-09 04:25:21

Tengo algo similar. Tengo muchos casos en los que lo hago:

thedict = {}
for item in ('foo', 'bar', 'baz'):
  mydict = thedict.get(item, {})
  mydict = get_value_for(item)
  thedict[item] = mydict

Pero yendo a muchos niveles profundos. Es el".get (item, {}) " esa es la clave, ya que hará otro diccionario si no hay uno ya. Mientras tanto, he estado pensando en formas de lidiar con esta mejor. En este momento, hay un montón de

value = mydict.get('foo', {}).get('bar', {}).get('baz', 0)

Así que en su lugar, hice:

def dictgetter(thedict, default, *args):
  totalargs = len(args)
  for i,arg in enumerate(args):
    if i+1 == totalargs:
      thedict = thedict.get(arg, default)
    else:
      thedict = thedict.get(arg, {})
  return thedict

Que tiene el mismo efecto si lo haces:

value = dictgetter(mydict, 0, 'foo', 'bar', 'baz')

Mejor? Eso creo.

 0
Author: uzi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-19 18:47:35

Solía usar esta función. es seguro, rápido, fácil de mantener.

def deep_get(dictionary, keys, default=None):
    return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)

Ejemplo:

>>> from functools import reduce
>>> def deep_get(dictionary, keys, default=None):
...     return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
...
>>> person = {'person':{'name':{'first':'John'}}}
>>> print (deep_get(person, "person.name.first"))
John
>>> print (deep_get(person, "person.name.lastname"))
None
>>> print (deep_get(person, "person.name.lastname", default="No lastname"))
No lastname
>>>
 0
Author: Yuda Prawira,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-23 14:27:01