2010-04-08 20 views
116

Grazie ad alcune persone fantastiche su SO, ho scoperto le possibilità offerte da collections.defaultdict, in particolare in leggibilità e velocità. Li ho usati per il successo.Livelli multipli di "collection.defaultdict" in Python

Ora vorrei implementare tre livelli di dizionari, i primi due sono defaultdict e il più basso è int. Non trovo il modo appropriato per farlo. Ecco il mio tentativo:

from collections import defaultdict 
d = defaultdict(defaultdict) 
a = [("key1", {"a1":22, "a2":33}), 
    ("key2", {"a1":32, "a2":55}), 
    ("key3", {"a1":43, "a2":44})] 
for i in a: 
    d[i[0]] = i[1] 

Ora questo funziona, ma quanto segue, che è il comportamento desiderato, non:

d["key4"]["a1"] + 1 

Ho il sospetto che avrei dichiarato da qualche parte che il secondo livello defaultdict è di tipo int, ma non ho trovato dove o come farlo.

Il motivo per cui sto usando defaultdict in primo luogo è evitare di dover inizializzare il dizionario per ogni nuova chiave.

Qualche suggerimento più elegante?

Grazie a Pythoneers!

risposta

244

Usa:

d = defaultdict(lambda: defaultdict(int)) 

Questo creerà una nuova defaultdict(int) ogni volta che una nuova chiave si accede in d.

+0

L'unico problema è che non decapiterà, il che significa che "multiprocessing" non è felice di inviare questi avanti e indietro. – Noah

+15

@Noah: si salverà se si utilizza una funzione di livello modulo denominata anziché una lambda. – interjay

+0

ovviamente, stupido. – Noah

10

Vedere la risposta di nosklo here per una soluzione più generale.

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 

Testing:

a = AutoVivification() 

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

print a 

Output:

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

Grazie per il link @ miles82 (e la modifica, @voyager). Quanto è poderoso e sicuro questo approccio? – Morlock

+0

Sfortunatamente questa soluzione non conserva la parte più comoda di defaultdict, che è il potere di scrivere qualcosa come D ['chiave'] + = 1 senza preoccuparsi dell'esistenza della chiave. Questa è la caratteristica principale che uso defaultdict per ... ma posso immaginare che i dizionari di approfondimento dinamico sono anche molto utili. – rschwieb

+1

@rschwieb puoi aggiungere il potere di scrivere + = 1 aggiungendo il metodo __add__. – spazm

3

Secondo la richiesta di @ rschwieb per D['key'] += 1, siamo in grado di espandere su previous sovrascrivendo inoltre definendo __add__ metodo, per rendere questo comportarsi più come un collections.Counter()

Prima __missing__ saranno chiamati a creare un nuovo vuoto valore, che sarà passato a __add__. Testiamo il valore, contando sui valori vuoti per essere False.

Vedere emulating numeric types per ulteriori informazioni sull'override.

from numbers import Number 


class autovivify(dict): 
    def __missing__(self, key): 
     value = self[key] = type(self)() 
     return value 

    def __add__(self, x): 
     """ override addition for numeric types when self is empty """ 
     if not self and isinstance(x, Number): 
      return x 
     raise ValueError 

    def __sub__(self, x): 
     if not self and isinstance(x, Number): 
      return -1 * x 
     raise ValueError 

Esempi:

>>> import autovivify 
>>> a = autovivify.autovivify() 
>>> a 
{} 
>>> a[2] 
{} 
>>> a 
{2: {}} 
>>> a[4] += 1 
>>> a[5][3][2] -= 1 
>>> a 
{2: {}, 4: 1, 5: {3: {2: -1}}} 

Invece di controllare argomento è un numero (molto non-python, amirite!) Potremmo fornire un valore predefinito 0 e quindi tentare l'operazione:

class av2(dict): 
    def __missing__(self, key): 
     value = self[key] = type(self)() 
     return value 

    def __add__(self, x): 
     """ override addition when self is empty """ 
     if not self: 
      return 0 + x 
     raise ValueError 

    def __sub__(self, x): 
     """ override subtraction when self is empty """ 
     if not self: 
      return 0 - x 
     raise ValueError 
+0

dovrebbe sollevare NotImplemented piuttosto che ValueError? – spazm

13

Un altro modo per fare una pickleable, defaultdict annidata è utilizzare un oggetto parziale invece di un lambda:

from functools import partial 
... 
d = defaultdict(partial(defaultdict, int)) 

Questo funzionerà perché la classe defaultdict è accessibile a livello globale a livello di modulo:

"You can't pickle a partial object unless the function [or in this case, class] it wraps is globally accessible ... under its __name__ (within its __module__)" -- Pickling wrapped partial functions

Problemi correlati