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


201

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 capa inferior a través de molestos 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, como tales:

{('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 iterar sobre los valores sea muy simple y natural, pero es más sintácticamente doloroso hacer cosas como agregaciones y mirar 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 como una jerarquía compleja. Podría envolver todo esto en una clase, pero parece que alguien podría haberlo hecho ya. Alternativamente, parece que podría haber algunas construcciones sintácticas realmente elegantes para hacer esto.

¿Cómo podría hacer esto mejor?

Anexo: Soy consciente, setdefault()pero en realidad no es una sintaxis limpia. Además, cada sub-diccionario que cree aún debe setdefault()configurarse manualmente.

Respuestas:


179

¿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.setdefaultdonde 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 dictsubclase 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 Vividictcada 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 Vividictmí 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.setdefaultfunciona 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 dictpara 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 setdefaultlas otras soluciones, y tengo en cada situación donde he necesitado este tipo de comportamiento.


Excelente respuesta! ¿Hay alguna forma de especificar una profundidad finita y un tipo de hoja para a Vividict? Por ejemplo, 3y listpara un dict de dict de dict de listas que podrían ser pobladas d['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.
Eric Duminil

@EricDuminil d['primary']['secondary'].setdefault('tertiary', []).append('element')- ?? Gracias por el cumplido, pero permítanme ser sincero, nunca lo uso __missing__, siempre lo uso setdefault. Probablemente debería actualizar mi conclusión / introducción ...
Aaron Hall

@AaronHall El comportamiento correcto es que el código debe crear un dict si es necesario. En este caso, anulando el valor asignado anterior.
nehem

@AaronHall ¿También me pueden ayudar a comprender qué se entiende por The bad lookup will remain in the dictionary.considerar esta solución? Muy apreciado. Thx
nehem

@AaronHall El problema con él fallaría setdefaultcuando 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 para get_nestedy otro para los set_nestedcuales aceptaba una referencia para dict y una lista de atributos anidados.
Nehem

188
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}}}

¿Alguien tiene este problema cuando se mudó a Python 3.x? stackoverflow.com/questions/54622935/…
jason

@jason picklees 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.
nosklo

¿Qué utilizas para almacenar estos objetos? Mi objeto de autovivificación contiene solo marcos de datos y cadenas de pandas.
Jason

@jason Dependiendo de los datos, me gusta usar JSON, archivos csv o incluso una sqlitebase de datos para almacenarlos.
nosklo

30

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

# yo dawg, i heard you liked dicts                                                                      
def yodict():
    return defaultdict(yodict)

2
@wberry: en realidad todo lo que necesitas es yodict = lambda: defaultdict(yodict).
Martineau

1
La versión aceptada es una subclase de dict, por lo que para ser completamente equivalentes tendríamos que x = Vdict(a=1, b=2)trabajar.
wberry

@wberry: Independientemente de lo que haya en la respuesta aceptada, ser una subclase dictno 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.
Martineau

24

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éelo en Python

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

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


44
YAML es definitivamente mi elección para ingresar muchos datos profundamente anidados (y archivos de configuración, maquetas de bases de datos, etc.). Si el OP no quiere archivos adicionales, simplemente use una cadena Python normal en algún archivo y analícelo con YAML.
kmelvn

Buen punto para crear cadenas YAML: este sería un enfoque mucho más limpio que usar el módulo "tempfile" repetidamente.
Pete

18

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.

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 puede recorrer un largo camino para crear un diseño similar a un almacén de datos sin los gastos generales de SQL.


14

Si el número de niveles de anidación es pequeño, lo uso collections.defaultdictpara 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

Utilizando defaultdictcomo esto evita un montón de desordenado setdefault(), get()etc.


+1: defaultdict es una de mis adiciones favoritas de todos los tiempos a Python. ¡No más .setdefault ()!
John Fouhy

8

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

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

Úselo 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"

Repetir 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 de forma recursiva todos estos defaultdicts a normales dict.

def dictify(d):
    for k,v in d.iteritems():
        if isinstance(v,defaultdict):
            d[k] = dictify(v)
    return dict(d)

7

Me parece setdefaultbastante útil; Comprueba si hay una clave presente y la agrega si no:

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

setdefaultsiempre 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:

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)

Me gusta esta solución, pero cuando intento: count.setdefault (a, {}). Setdefault (b, {}). Setdefault (c, 0) + = 1 obtengo "expresión ilegal para asignación aumentada"
dfrankow

6

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"'))

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


5

collections.defaultdictse puede subclasificar para hacer 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)

1
Esta es la respuesta que más se acerca a lo que estaba buscando. Pero, idealmente, habría todo tipo de funciones auxiliares, por ejemplo, walk_keys () o similares. Me sorprende que no haya nada en las bibliotecas estándar para hacer esto.
YGA

4

En cuanto a "molestos bloques try / catch":

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

rendimientos

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

Puede usar esto para convertir 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

defaultdict() ¡es tu amigo!

Para un diccionario bidimensional puedes hacer:

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

Para más dimensiones puedes:

d = defaultdict(lambda :defaultdict(defaultdict))
d[1][2][3] = 4

Esta respuesta funciona solo para tres niveles en el mejor de los casos. Para niveles arbitrarios, considere esta respuesta .
Acumenus

3

Para iterar fácilmente sobre su diccionario anidado, ¿por qué no simplemente 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 tiene su 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 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í:

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 considerar el uso de collections.defaultdict , que forma 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']

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.


2

Me gusta la idea de envolver esto en una clase e implementar __getitem__y __setitem__tal que implementaron 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 quisieras ponerte elegante, también podrías implementar algo como:

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

pero sobre todo creo que tal cosa sería muy divertida de implementar: D


Creo que es una mala idea: nunca se puede predecir la sintaxis de las claves. Todavía anularías getitem y setitem pero harías que tuplas.
YGA

3
@YGA Probablemente tengas razón, pero es divertido pensar en implementar mini idiomas como este.
Aaron Maenpaa

1

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.


1
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

>>>

Editar: ahora devuelve diccionarios cuando se consulta con comodines ( None), y valores individuales de lo contrario.


¿Por qué devolver listas? Parece que debería devolver un diccionario (para que sepa lo que representa cada número) o una suma (ya que eso es todo lo que realmente puede hacer con la lista).
Ben Blank

0

Tengo una cosa similar en marcha. Tengo muchos casos en los que 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 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

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

Entonces, en cambio, 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

Lo que tiene el mismo efecto si haces:

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

¿Mejor? Creo que sí.


0

Puede usar la recursión en lambdas y defaultdict, no es necesario 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})})})

0

Solía ​​usar esta función. Es seguro, rápido y 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
>>>
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.