2013-07-08 5 views
13

Ein kleiner Ärger mit dict.setdefault ist, dass es immer sein zweites Argument auswertet (wenn natürlich gegeben), auch wenn das erste das erste Argument bereits ein Schlüssel im Wörterbuch ist.Wie implementiert man einen Lazy-Setdefault?

Zum Beispiel:

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

Dies erzeugt ouptut wie folgt aus:

noisy_default: returning 4063267 
4063267 
noisy_default: returning 628989 
4063267 

Da die letzte Zeile bestätigt, die zweite Ausführung von noisy_default unnötig ist, von diesem Punkt, da der Schlüssel 1 ist bereits vorhanden in d (mit Wert 4063267).

Ist es möglich, eine Unterklasse von dict zu implementieren, deren setdefault Methode das zweite Argument faul auswertet?


EDIT:

Im Folgenden ist eine Implementierung inspiriert von BrenBarn Kommentar und Antwort Pavel Anossov. Während ich dabei war, habe ich eine faule Version von get implementiert, da die zugrunde liegende Idee im Wesentlichen die gleiche ist. Jetzt

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

, das Snippet

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

erzeugt eine Ausgabe wie folgt aus:

noisy_default: returning 5025427 
5025427 
5025427 

Beachten Sie, dass das zweite Argument zu d.setdefault oben ist jetzt eine aufrufbare, kein Funktionsaufruf. Wenn das zweite Argument zu LazyDict.get oder LazyDict.setdefault nicht aufrufbar ist, verhalten sie sich genauso wie die entsprechenden dict Methoden.

Wenn man einen aufrufbaren als Standardwert passieren will sich (das heißt, nicht gemeint genannt werden), oder, wenn das aufrufbare aufgerufen werden erfordert Argumente, prepend lambda: an die entsprechende Argument. ZB:

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

Sie können nicht das zweite Argument nicht bewerten. Sie müssten dieses Argument in eine Funktion (z. B. mit "Lambda") einbinden und dann mit "setdefault" die Funktion nur bei Bedarf aufrufen. – BrenBarn

+0

Kann ich vorschlagen, dass Sie '* args, ** kwargs' zu den Signaturen von' lazetget', 'lazysetdefault' und dem Aufruf von' thunk() 'hinzufügen? Dies würde Ihren faulen Sachen erlauben, Parameter zu nehmen. z.B. 'lbd.lazysetdefault ('total', sum, [1, 2, 3, 4], start = 2)' – Hounshell

Antwort

6

:

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

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

Diejenigen, die nicht auf die Idee gefällt vorran get und setdefault und/oder die daraus resultierende Notwendigkeit Kündbarkeit zu testen, usw., diese Version stattdessen verwenden können Nein, die Auswertung der Argumente erfolgt vor dem Aufruf. Sie können eine setdefault ähnliche Funktion implementieren, die ein aufrufbares Argument als zweites Argument verwendet und sie nur aufruft, wenn sie benötigt wird.

9

Dies kann auch mit defaultdict erreicht werden. Es wird mit einer Callable instanziiert, die dann aufgerufen wird, wenn auf ein nicht existierendes Element zugegriffen wird.

from collections import defaultdict 

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

Der Nachteil mit defaultdict ist, dass die abrufbare keine Argumente bekommen, so können Sie den Standardwert aus dem Schlüssel nicht ableiten, wie Sie mit dict.setdefault können. Dies kann durch zwingende __missing__ in einer Unterklasse gemildert werden:

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 

Weitere Informationen finden Sie im collections Modul.

4

Sie können in einem Einzeiler tun, dass ein ternären Operator:

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

Wenn Sie sicher sind, dass die cache falsy Werte nie gespeichert werden, können Sie es ein wenig vereinfachen:

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

Wenn Sie 'key in dict' ankreuzen, ist es sinnlos, 'setdeault' zu verwenden. – user1685095

+1

Dies erfordert es suche 'key' zweimal in' cache'. Das ist keine große Sache für dict basierend auf Hash-Map, macht aber trotzdem keinen Sinn. –

+0

@ user1685095 Wenn Sie setdefault nicht aufrufen, wird der Cache nicht aktualisiert. setdefault setzt den leeren Cache und gibt gleichzeitig seinen Wert zurück –