2013-07-08 11 views
13

Una piccola seccatura con dict.setdefault è che valuta sempre il suo secondo argomento (quando viene dato, ovviamente), anche quando il primo il primo argomento è già una chiave nel dizionario.Come implementare un setdefault pigro?

Ad esempio:

import random 
def noisy_default(): 
    ret = random.randint(0, 10000000) 
    print 'noisy_default: returning %d' % ret 
    return ret 

d = dict() 
print d.setdefault(1, noisy_default()) 
print d.setdefault(1, noisy_default()) 

Questo produce ouptut come la seguente:

noisy_default: returning 4063267 
4063267 
noisy_default: returning 628989 
4063267 

Come ultima linea conferma, la seconda esecuzione di noisy_default è necessaria perché a questo punto il tasto 1 è già presente in d (con valore 4063267).

È possibile implementare una sottoclasse di dict il cui metodo setdefault valuta pigramente il secondo argomento?


EDIT:

Di seguito è un'implementazione ispirato dal commento di BrenBarn e la risposta di Pavel Anossov. Mentre lo facevo, sono andato avanti e ho implementato anche una versione lenta di get, dato che l'idea di base è essenzialmente la stessa.

class LazyDict(dict): 
    def get(self, key, thunk=None): 
     return (self[key] if key in self else 
       thunk() if callable(thunk) else 
       thunk) 


    def setdefault(self, key, thunk=None): 
     return (self[key] if key in self else 
       dict.setdefault(self, key, 
           thunk() if callable(thunk) else 
           thunk)) 

momento, frammento

d = LazyDict() 
print d.setdefault(1, noisy_default) 
print d.setdefault(1, noisy_default) 

produce output del tipo:

noisy_default: returning 5025427 
5025427 
5025427 

noti che il secondo parametro di d.setdefault sopra è ora un richiamabile, non una chiamata di funzione.

Quando il secondo argomento su LazyDict.get o LazyDict.setdefault non è un chiamabile, si comportano allo stesso modo dei corrispondenti metodi dict.

Se si vuole passare un richiamabile come valore predefinito stessa (cioè, non destinata ad essere chiamato), o se il richiamabile essere chiamato richiede argomenti, anteporre lambda: all'argomento appropriato. Ad esempio:

d1.setdefault('div', lambda: div_callback) 

d2.setdefault('foo', lambda: bar('frobozz')) 

Coloro che non amano l'idea di override get e setdefault, e/o la conseguente necessità di testare per l'esigibilità, ecc, può usare questa versione invece:

class LazyButHonestDict(dict): 
    def lazyget(self, key, thunk=lambda: None): 
     return self[key] if key in self else thunk() 


    def lazysetdefault(self, key, thunk=lambda: None): 
     return (self[key] if key in self else 
       self.setdefault(key, thunk())) 
+0

Non è possibile farlo non valutare il secondo argomento. Quello che dovresti fare è racchiudere quell'argomento in una funzione (ad es. Con 'lambda') e poi chiamare' setdefault' la funzione solo se necessario. – BrenBarn

+0

Posso suggerire di aggiungere '* args, ** kwargs' alle firme di' lazyget', 'lazysetdefault' e la chiamata a' thunk() '? Ciò consentirebbe ai tuoi oggetti pigri di prendere parametri. per esempio. 'lbd.lazysetdefault ('total', sum, [1, 2, 3, 4], start = 2)' – Hounshell

risposta

6

No, la valutazione degli argomenti avviene prima della chiamata. È possibile implementare una funzione simile a setdefault che accetta un callable come secondo argomento e la chiama solo se è necessaria.

9

Questo può essere eseguito anche con defaultdict. Viene istanziato con un callable che viene poi chiamato quando si accede a un elemento inesistente.

from collections import defaultdict 

d = defaultdict(noisy_default) 
d[1] # noise 
d[1] # no noise 

L'avvertenza con defaultdict è che il callable ottiene senza argomenti, quindi non è possibile ricavare il valore predefinito dalla chiave, come si potrebbe con dict.setdefault. Questo può essere mitigato da esigenze imperative __missing__ in una sottoclasse:

from collections import defaultdict 

class defaultdict2(defaultdict): 
    def __missing__(self, key): 
     value = self.default_factory(key) 
     self[key] = value 
     return value 

def noisy_default_with_key(key): 
    print key 
    return key + 1 

d = defaultdict2(noisy_default_with_key) 
d[1] # prints 1, sets 2, returns 2 
d[1] # does not print anything, does not set anything, returns 2 

Per ulteriori informazioni, vedere il modulo collections.

4

È possibile farlo in un one-liner utilizzando un operatore ternario:

value = cache[key] if key in cache else cache.setdefault(key, func(key)) 

Se si è certi che il cache sarà mai memorizzare i valori falsy, è possibile semplificare un po ':

value = cache.get(key) or cache.setdefault(key, func(key)) 
+1

Se stai controllando 'key in dict' non ha senso usare' setdeault' – user1685095

+1

Ciò richiederà cerca 'key' in' cache' due volte. Il che non è un grosso problema per il dt basato su Hash-Map, ma non è ancora così sensato. –

+0

@ user1685095 Se non si chiama setdefault, la cache non verrà aggiornata. setdefault sta sia impostando la cache vuota sia restituendo il suo valore allo stesso tempo –