Qual è il modo migliore per implementare dizionari nidificati?


201

Ho una struttura di dati che essenzialmente equivale a un dizionario nidificato. Diciamo che assomiglia a questo:

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

Ora, mantenere e creare questo è piuttosto doloroso; ogni volta che ho un nuovo stato / contea / professione devo creare i dizionari di livello inferiore tramite blocchi di prova / cattura odiosi. Inoltre, devo creare fastidiosi iteratori nidificati se voglio esaminare tutti i valori.

Potrei anche usare le tuple come chiavi, come queste:

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

Questo rende l'iterazione dei valori molto semplice e naturale, ma è più sintatticamente doloroso fare cose come aggregazioni e guardare sottoinsiemi del dizionario (ad esempio se voglio solo andare stato per stato).

Fondamentalmente, a volte voglio pensare a un dizionario nidificato come un dizionario semplice, ea volte voglio pensarlo davvero come una gerarchia complessa. Potrei concludere tutto in una classe, ma sembra che qualcuno potrebbe averlo già fatto. In alternativa, sembra che ci possano essere delle costruzioni sintattiche davvero eleganti per farlo.

Come potrei farlo meglio?

Addendum: ne sono consapevole setdefault()ma in realtà non crea sintassi pulita. Inoltre, ogni sotto-dizionario creato deve essere setdefault()impostato manualmente.

Risposte:


179

Qual è il modo migliore per implementare dizionari nidificati in Python?

Questa è una cattiva idea, non farlo. Invece, usa un dizionario normale e usa dict.setdefaultdove apropos, quindi quando mancano le chiavi durante il normale utilizzo ottieni il previsto KeyError. Se insisti per ottenere questo comportamento, ecco come spararti al piede:

Implementare __missing__una dictsottoclasse per impostare e restituire una nuova istanza.

Questo approccio è disponibile (e documentato) da Python 2.5 e (particolarmente prezioso per me) stampa piuttosto come un normale dict , invece della brutta stampa di un dict predefinito autovivificato:

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]è sul lato sinistro del compito, quindi non c'è ricorsione qui.)

e dire che hai alcuni dati:

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}

Ecco il nostro codice di utilizzo:

vividict = Vividict()
for (state, county, occupation), number in data.items():
    vividict[state][county][occupation] = number

E adesso:

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

Critica

Una critica a questo tipo di contenitore è che se l'utente scrive erroneamente una chiave, il nostro codice potrebbe fallire silenziosamente:

>>> vividict['new york']['queens counyt']
{}

E inoltre ora avremmo una contea errata nei nostri dati:

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

Spiegazione:

Forniamo solo un'altra istanza nidificata della nostra classe Vividictogni volta che si accede a una chiave ma manca. (Restituire l'assegnazione di valore è utile perché ci evita inoltre di chiamare il getter sul dict e, sfortunatamente, non possiamo restituirlo mentre viene impostato.)

Nota, queste sono la stessa semantica della risposta più votata, ma a metà delle righe di codice - l'implementazione di 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

Dimostrazione di utilizzo

Di seguito è riportato solo un esempio di come questo dict possa essere facilmente utilizzato per creare al volo una struttura nidificata. Questo può creare rapidamente una struttura ad albero gerarchica tanto profonda quanto potresti voler andare.

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)

Quali uscite:

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

E come mostra l'ultima riga, stampa in modo bello e in ordine per l'ispezione manuale. Ma se vuoi ispezionare visivamente i tuoi dati, l'implementazione __missing__per impostare una nuova istanza della sua classe sulla chiave e restituirla è una soluzione molto migliore.

Altre alternative, per contrasto:

dict.setdefault

Anche se chi lo ascolta pensa che non sia pulito, lo trovo preferibile a Vividictme stesso.

d = {} # or dict()
for (state, county, occupation), number in data.items():
    d.setdefault(state, {}).setdefault(county, {})[occupation] = number

e adesso:

>>> 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 errore di ortografia fallirebbe rumorosamente e non ingombrerebbe i nostri dati con informazioni errate:

>>> d['new york']['queens counyt']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'queens counyt'

Inoltre, penso che setdefault funzioni alla grande se usato nei loop e non sai cosa otterrai per le chiavi, ma l'uso ripetitivo diventa abbastanza oneroso e non penso che nessuno vorrebbe mantenere il seguente:

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', {})

Un'altra critica è che setdefault richiede una nuova istanza sia che venga usata o meno. Tuttavia, Python (o almeno CPython) è piuttosto intelligente nella gestione di nuove istanze non utilizzate e non referenziate, ad esempio, riutilizza la posizione in memoria:

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

Un decreto predefinito auto-vivificato

Questa è un'implementazione dall'aspetto pulito e l'uso in uno script su cui non stai ispezionando i dati sarebbe utile quanto implementare __missing__:

from collections import defaultdict

def vivdict():
    return defaultdict(vivdict)

Ma se hai bisogno di ispezionare i tuoi dati, i risultati di un predefinito predefinito auto-vivificato popolato con i dati nello stesso modo assomigliano a questo:

>>> 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>, {})})})

Questo output è abbastanza inelegante e i risultati sono piuttosto illeggibili. La soluzione generalmente fornita è quella di riconvertire ricorsivamente in un dict per l'ispezione manuale. Questa soluzione non banale viene lasciata come esercizio per il lettore.

Prestazione

Infine, diamo un'occhiata alle prestazioni. Sto sottraendo i costi dell'istanza.

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

Basato sulle prestazioni, dict.setdefaultfunziona al meglio. Lo consiglio vivamente per il codice di produzione, nei casi in cui ti preoccupi della velocità di esecuzione.

Se ne hai bisogno per un uso interattivo (in un notebook IPython, forse), le prestazioni non contano davvero: in tal caso, preferirei Vividict per la leggibilità dell'output. Rispetto all'oggetto AutoVivification (che utilizza __getitem__invece di __missing__, creato per questo scopo) è di gran lunga superiore.

Conclusione

L'implementazione __missing__su una sottoclasse dictper impostare e restituire una nuova istanza è leggermente più difficile delle alternative ma ha i vantaggi di

  • istanza facile
  • facile popolazione di dati
  • facile visualizzazione dei dati

e poiché è meno complicato e più performante della modifica __getitem__, dovrebbe essere preferito a quel metodo.

Tuttavia, ha degli svantaggi:

  • Le ricerche sbagliate falliranno silenziosamente.
  • La ricerca errata rimarrà nel dizionario.

Quindi personalmente preferisco setdefaultle altre soluzioni e ho in ogni situazione in cui ho avuto bisogno di questo tipo di comportamento.


Risposta eccellente! C'è un modo per specificare una profondità finita e un tipo di foglia per un Vividict? Ad esempio 3e listper un dict of dict of dict di elenchi che potrebbero essere popolati con d['primary']['secondary']['tertiary'].append(element). Potrei definire 3 classi diverse per ogni profondità, ma mi piacerebbe trovare una soluzione più pulita.
Eric Duminil,

@EricDuminil d['primary']['secondary'].setdefault('tertiary', []).append('element')- ?? Grazie per il complimento, ma lasciatemi essere onesto - non lo uso mai realmente __missing__- lo uso sempre setdefault. Probabilmente dovrei aggiornare la mia conclusione / introduzione ...
Aaron Hall

@AaronHall Il comportamento corretto è che il codice dovrebbe creare un dict se necessario. In questo caso, sovrascrivendo il valore assegnato in precedenza.
Nehem

@AaronHall Puoi anche aiutarmi a capire cosa si intende The bad lookup will remain in the dictionary.quando sto considerando di utilizzare questa soluzione ?. Molto apprezzato. Thx
nehem

@AaronHall Il problema con esso fallirebbe setdefaultquando nidificava più di due livelli di profondità. Sembra che nessuna struttura in Python possa offrire una vera vivificazione come descritto. Ho dovuto accontentarmi di due metodi affermativi uno per get_nested& uno per i set_nestedquali accettare un riferimento per dict e un elenco di attributi nidificati.
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

test:

a = AutoVivification()

a[1][2][3] = 4
a[1][3][3] = 5
a[1][2]['test'] = 6

print a

Produzione:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}

Qualcuno ha questo problema quando si sono spostati su Python 3.x? stackoverflow.com/questions/54622935/…
jason

@jason pickleè terribile tra le versioni di Python. Evita di usarlo per archiviare i dati che desideri conservare. Usalo solo per cache e cose che puoi scaricare e rigenerare a piacimento. Non come metodo di archiviazione o serializzazione a lungo termine.
nosklo,

Cosa usi per conservare questi oggetti? Il mio oggetto di autovivificazione contiene solo frame di dati e stringhe di Panda.
Jason,

@jason A seconda dei dati, mi piace usare JSON, file CSV o anche un sqlitedatabase per memorizzarli.
nosklo,

30

Solo perché non ne ho visto uno così piccolo, ecco un dict che diventa nidificato come vuoi, senza sudore:

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

2
@wberry: in realtà tutto ciò che serve è yodict = lambda: defaultdict(yodict).
martineau,

1
La versione accettata è una sottoclasse di dict, quindi per essere del tutto equivalente dovremmo x = Vdict(a=1, b=2)lavorare.
wberry,

@wberry: Indipendentemente da ciò che è nella risposta accettata, essere una sottoclasse dictnon era un requisito dichiarato dal PO, che chiedeva solo il "modo migliore" per implementarli - e inoltre, non dovrebbe / non dovrebbe importa così tanto in Python comunque.
martineau,

24

È possibile creare un file YAML e leggerlo utilizzando PyYaml .

Passaggio 1: creare un file YAML, "loyment.yml ":

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

Passaggio 2: leggilo in Python

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

e ora my_shnazzy_dictionaryha tutti i tuoi valori. Se è necessario farlo al volo, è possibile creare lo YAML come stringa e inserirlo yaml.safe_load(...).


4
YAML è sicuramente la mia scelta per inserire molti dati profondamente nidificati (e file di configurazione, modelli di database, ecc ...). Se l'OP non vuole file extra in giro, basta usare una normale stringa Python in alcuni file e analizzarla con YAML.
kmelvn,

Un buon punto per la creazione di stringhe YAML: sarebbe un approccio molto più pulito rispetto all'utilizzo ripetuto del modulo "tempfile".
Pete,

18

Dato che hai un disegno a stella, potresti voler strutturarlo più come una tabella relazionale e meno come un dizionario.

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

Questo genere di cose può fare molto per creare un design simile al data warehouse senza le spese generali di SQL.


14

Se il numero di livelli di nidificazione è piccolo, lo uso collections.defaultdictper questo:

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

Utilizzando defaultdictcome questo evita un sacco di disordinato setdefault(), get()ecc


+1: defaultdict è una delle mie aggiunte preferite di tutti i tempi a Python. Non più .setdefault ()!
John Fouhy,

8

Questa è una funzione che restituisce un dizionario nidificato di profondità arbitraria:

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

Usalo in questo modo:

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"

Scorri tutto con qualcosa del genere:

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)

Questo stampa:

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

Alla fine potresti volerlo fare in modo che non sia possibile aggiungere nuovi elementi al dict. È facile convertire ricorsivamente tutti questi messaggi defaultdictin messaggi normali dict.

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

7

Trovo setdefaultabbastanza utile; Verifica se è presente una chiave e la aggiunge in caso contrario:

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

setdefaultrestituisce sempre la chiave pertinente, quindi in realtà stai aggiornando i valori di " d" sul posto.

Quando si tratta di iterare, sono sicuro che potresti scrivere un generatore abbastanza facilmente se uno non esiste già in 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)

Mi piace questa soluzione, ma quando provo: count.setdefault (a, {}). Setdefault (b, {}). Setdefault (c, 0) + = 1 Ottengo "espressione illegale per compito aumentato"
dfrankow

6

Come altri hanno suggerito, un database relazionale potrebbe essere più utile per te. È possibile utilizzare un database sqlite3 in memoria come struttura dati per creare tabelle e quindi interrogarle.

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

Questo è solo un semplice esempio. È possibile definire tabelle separate per stati, contee e titoli di lavoro.


5

collections.defaultdictpuò essere suddiviso in classi per creare un dict nidificato. Quindi aggiungere eventuali metodi di iterazione utili a quella classe.

>>> 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
Questa è la risposta che si avvicina di più a quello che stavo cercando. Ma idealmente ci sarebbero tutti i tipi di funzioni di supporto, ad esempio walk_keys () o simili. Sono sorpreso che non ci sia nulla nelle librerie standard per farlo.
YGA

4

Per quanto riguarda "odiosi blocchi try / catch":

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

i rendimenti

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

Puoi usarlo per convertire dal tuo formato di dizionario piatto in formato strutturato:

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() È tuo amico!

Per un dizionario bidimensionale puoi fare:

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

Per più dimensioni puoi:

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

Questa risposta funziona al meglio solo per tre livelli. Per livelli arbitrari, considera questa risposta .
Acumenus,

3

Per una facile iterazione sul dizionario nidificato, perché non scrivere un semplice generatore?

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
                }

Quindi, se hai il tuo dizionario nidificato compilato, iterarlo diventa semplice:

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

Ovviamente il tuo generatore può produrre qualunque formato di dati ti sia utile.

Perché stai usando provare a catturare i blocchi per leggere l'albero? È abbastanza facile (e probabilmente più sicuro) interrogare se esiste una chiave in un dict prima di provare a recuperarla. Una funzione che utilizza clausole di protezione potrebbe essere simile alla seguente:

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

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

Oppure, un metodo forse un po 'dettagliato, è usare il metodo get:

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

Ma per un modo un po 'più conciso, potresti voler guardare usando un collection.defaultdict , che fa parte della libreria standard da 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']

Sto facendo ipotesi sul significato della struttura dei tuoi dati qui, ma dovrebbe essere facile adattarsi a ciò che realmente vuoi fare.


2

Mi piace l'idea di racchiuderlo in una classe e implementarlo __getitem__e in modo __setitem__tale che abbiano implementato un semplice linguaggio di query:

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

Se vuoi essere sofisticato, puoi anche implementare qualcosa del tipo:

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

ma soprattutto penso che una cosa del genere sarebbe davvero divertente da implementare: D


Penso che sia una cattiva idea: non puoi mai prevedere la sintassi dei tasti. Dovresti comunque scavalcare getitem e setitem ma farli prendere le tuple.
YGA

3
@YGA Probabilmente hai ragione, ma è divertente pensare a implementare mini linguaggi come questo.
Aaron Maenpaa,

1

A meno che il tuo set di dati non rimanga piuttosto piccolo, potresti prendere in considerazione l'utilizzo di un database relazionale. Farà esattamente quello che vuoi: rendere facile aggiungere conteggi, selezionare sottoinsiemi di conteggi e persino conteggi aggregati per stato, contea, occupazione o qualsiasi combinazione di questi.


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

Esempio:

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

>>>

Modifica: ora restituisce dizionari quando si esegue una query con caratteri jolly ( None) e in caso contrario valori singoli.


Perché restituire le liste? Sembra che dovrebbe restituire un dizionario (quindi sai cosa rappresenta ogni numero) o una somma (dato che è tutto ciò che puoi veramente fare con l'elenco).
Ben Blank,

0

Ho una cosa simile in corso. Ho molti casi in cui lo faccio:

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

Ma andando a molti livelli di profondità. È il ".get (item, {})" che è la chiave in quanto creerà un altro dizionario se non ce n'è già uno. Nel frattempo, ho pensato a modi per affrontarlo meglio. In questo momento, ce ne sono molti

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

Quindi, invece, ho fatto:

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

Che ha lo stesso effetto se lo fai:

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

Meglio? Credo di si.


0

È possibile utilizzare la ricorsione in lambdas e defaultdict, non è necessario definire i nomi:

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

Ecco un esempio:

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

Usavo questa funzione. è sicuro, rapido, facilmente gestibile.

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)

Esempio :

>>> 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
>>>
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.