Qual è il modo migliore per implementare dizionari nidificati in Python?
Questa è una cattiva idea, non farlo. Invece, usa un dizionario normale e usa dict.setdefault
dove 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 dict
sottoclasse 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 Vividict
ogni 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 Vividict
me 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.setdefault
funziona 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 dict
per 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 setdefault
le altre soluzioni e ho in ogni situazione in cui ho avuto bisogno di questo tipo di comportamento.
Vividict
? Ad esempio3
elist
per un dict of dict of dict di elenchi che potrebbero essere popolati cond['primary']['secondary']['tertiary'].append(element)
. Potrei definire 3 classi diverse per ogni profondità, ma mi piacerebbe trovare una soluzione più pulita.