2012-08-06 3 views
6

Die folgende Funktion soll als Dekorator verwendet werden, der die Ergebnisse bereits berechneter Werte speichert. Wenn das Argument bereits vor berechnet wurde, liefert die Funktion den Wert im cache Wörterbuch gespeichert:Wie können Daten über mehrere Aufrufe von dekorierten Funktionen hinweg bestehen bleiben?

def cached(f): 
    f.cache = {} 
    def _cachedf(*args): 
     if args not in f.cache: 
      f.cache[args] = f(*args) 

     return f.cache[args] 

    return _cachedf 

I (versehentlich) erkannte, dass cache kein Attribut der Funktion Objekt sein muss. Als eine Frage der Fakten, der folgende Code funktioniert auch:

def cached(f): 
    cache = {} # <---- not an attribute this time! 
    def _cachedf(*args): 
     if args not in cache: 
      cache[args] = f(*args) 

     return cache[args] 
    return _cachedf 

ich eine harte Zeit habe zu verstehen, wie kann das cache Objekt über mehrere Anrufe persistent. Ich habe versucht, mehrere zwischengespeicherte Funktionen mehrmals aufzurufen und konnte keine Konflikte oder Probleme finden.

Kann mir bitte jemand helfen, zu verstehen, wie die cache Variable noch existiert, auch nachdem die _cachedf Funktion zurückgegeben wird?

Antwort

11

Sie erstellen hier eine closure: Die Funktion _cachedf() schließt über die Variable cache aus dem umschließenden Scope. Dies hält cache am Leben, solange das Funktionsobjekt lebt.

bearbeiten: Vielleicht sollte ich noch ein paar Details addieren, wie dies in Python funktioniert und wie CPython dies implementiert.

Blick Lassen Sie sich auf einem einfacheres Beispiel:

def f(): 
    a = [] 
    def g(): 
     a.append(1) 
     return len(a) 
    return g 

Beispiel für die Verwendung in dem interaktiven Interpreter

>>> h = f() 
>>> h() 
1 
>>> h() 
2 
>>> h() 
3 

Beim Übersetzen des Moduls enthält die Funktion f(), die Compiler sieht, dass die Funktion g() Referenzen der Name a aus dem umschließenden Umfang und speichert diese externe Referenz im Code Objekt cor Antwort auf die Funktion f() (speziell fügt es die Name a zu f.__code__.co_cellvars hinzu).

Was passiert also, wenn die Funktion f() aufgerufen wird? Die erste Zeile erstellt ein neues Listenobjekt und bindet es an den Namen a. Die nächste Zeile erstellt ein neues Funktionsobjekt (mit einem Code-Objekt, das während erstellt wurde, kompiliert das Modul) und bindet es an den Namen g. Der Rumpf von g() wird zu diesem Zeitpunkt nicht ausgeführt, und schließlich wird das Funciton-Objekt zurückgegeben.

Da das Code-Objekt von f() eine Notiz hat, dass der Name a durch lokale Funktionen referenziert ist, wird eine „Zelle“ für diesen Namen erstellt, wenn f() eingegeben wird. Diese Zelle enthält den Verweis auf die aktuelle Liste Objekt a ist gebunden, und die Funktion g() erhält einen Verweis auf diese Zelle. Auf diese Weise werden das Listenobjekt und die Zelle selbst dann am Leben gehalten, wenn die Funktion f() beendet wird.

+0

Vielen Dank für die Erklärung, Ihre Bearbeitung macht die Dinge sehr klar. Ich frage mich, um der Interna von (C) Python zu lernen, ob es möglich ist, auf die "Zelle" zuzugreifen, die Sie in Ihrem letzten Absatz erwähnt haben, durch Inspektion oder etwas Ähnliches? – rahmu

+0

@rahmu: Ich korrigierte einen Fehler in der Erklärung (die sich nicht zu viel ändert). Leider sind die Zellen für Python-Code vollständig transparent und werden immer durch das Objekt ersetzt, auf das sie verweisen, so dass sie nicht untersucht werden können. –

3

Kann mir bitte jemand helfen, zu verstehen, wie die Cache-Variable noch existiert, auch nachdem die _cachedf-Funktion zurückgegeben wurde?

Es hat mit Pythons Referenzzählung Garbage Collector zu tun. Die Variable cache bleibt erhalten und verfügbar, da die Funktion _cachedf einen Verweis darauf hat und der Aufrufer an cached einen Verweis darauf hat. Wenn Sie die Funktion erneut aufrufen, verwenden Sie immer noch das gleiche Funktionsobjekt, das ursprünglich erstellt wurde. Daher haben Sie weiterhin Zugriff auf den Cache.

Sie werden den Cache nicht verlieren, bis alle Verweise darauf zerstört sind. Sie können dazu den Operator del verwenden.

Zum Beispiel:

>>> import time 
>>> def cached(f): 
...  cache = {} # <---- not an attribute this time! 
...  def _cachedf(*args): 
...   if args not in cache: 
...    cache[args] = f(*args) 
...   return cache[args] 
...  return _cachedf 
...  
... 
>>> def foo(duration): 
...  time.sleep(duration) 
...  return True 
...  
... 
>>> bob = cached(foo) 
>>> bob(2) # Takes two seconds 
True 
>>> bob(2) # returns instantly 
True 
>>> del bob # Deletes reference to bob (aka _cachedf) which holds ref to cache 
>>> bob = cached(foo) 
>>> bob(2) # takes two seconds 
True 
>>> 

Für das Protokoll, was Sie versuchen Memoization genannt acheive ist, und es gibt eine vollständigere memoizing Dekorateur aus dem decorator pattern page zur Verfügung, die die gleiche Sache tut, aber eine mit Dekorateur Klasse. Ihr Code und der klassenbasierte Dekorator sind im Wesentlichen identisch, wobei der klassenbasierte Dekorator vor dem Speichern nach der Hash-Fähigkeit sucht.


Edit (2017.02.02): @SiminJie kommentiert, dass cached(foo)(2) zieht immer eine Verzögerung.

Dies liegt daran, cached(foo) gibt eine neue Funktion mit einem neuen Cache zurück. Wenn cached(foo)(2) aufgerufen wird, wird ein neuer (leerer) Cache erstellt und dann wird die zwischengespeicherte Funktion sofort aufgerufen.

Da der Cache leer ist und den Wert nicht finden kann, wird die zugrunde liegende Funktion erneut ausgeführt. Stattdessen tun Sie cached_foo = cached(foo) und rufen Sie dann cached_foo(2) mehrmals. Dies verursacht nur die Verzögerung für den ersten Anruf. wenn auch als Dekorateur verwendet, wird es wie erwartet:

@cached 
def my_long_function(arg1, arg2): 
    return long_operation(arg1,arg2) 

my_long_function(1,2) # incurs delay 
my_long_function(1,2) # doesn't 

Wenn Sie mit Dekorateure nicht vertraut sind, nehmen Sie einen Blick auf this answer zu verstehen, was die oben genannten Code bedeutet.

+0

Wie funktioniert das für Python Decorator? Jedes Mal, wenn ich 'cached (foo) (2)' aufrufe, speichert es das Ergebnis nicht und schläft zwei Sekunden. Bezieht sich jeder Aufruf der dekorierten Funktion auf den gleichen Dekorateur? –

+0

@SiminJie - Siehe meine zusätzliche Bearbeitung der Antwort. – brice

Verwandte Themen