Gibt es einen Dekorateur, der Funktionsrückgabewerte einfach zwischenspeichert?


157

Folgendes berücksichtigen:

@property
def name(self):

    if not hasattr(self, '_name'):

        # expensive calculation
        self._name = 1 + 1

    return self._name

Ich bin neu, aber ich denke, das Caching könnte in einen Dekorateur zerlegt werden. Nur habe ich so etwas nicht gefunden;)

PS: Die tatsächliche Berechnung hängt nicht von veränderlichen Werten ab


Es gibt vielleicht einen Dekorateur, der über solche Fähigkeiten verfügt, aber Sie haben nicht genau angegeben, was Sie wollen. Welche Art von Caching-Backend verwenden Sie? Und wie wird der Wert eingegeben? Ich gehe von Ihrem Code aus, dass das, wonach Sie wirklich fragen, eine zwischengespeicherte schreibgeschützte Eigenschaft ist.
David Berger

Es gibt Memo-Dekorateure, die das ausführen, was Sie als "Caching" bezeichnen. Sie arbeiten normalerweise an Funktionen als solche (ob sie Methoden werden sollen oder nicht), deren Ergebnisse von ihren Argumenten abhängen (nicht von veränderlichen Dingen wie Selbst! -), und führen daher ein separates Memo-Diktat.
Alex Martelli

Antworten:


206

Ab Python 3.2 gibt es einen eingebauten Dekorator:

@functools.lru_cache(maxsize=100, typed=False)

Decorator zum Umschließen einer Funktion mit einem Memoizing Callable, das bis zu der maximalen Größe der letzten Anrufe speichert. Dies kann Zeit sparen, wenn eine teure oder E / A-gebundene Funktion regelmäßig mit denselben Argumenten aufgerufen wird.

Beispiel eines LRU-Cache zur Berechnung von Fibonacci-Zahlen :

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

>>> print([fib(n) for n in range(16)])
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]

>>> print(fib.cache_info())
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)

Wenn Sie mit Python 2.x nicht weiterkommen, finden Sie hier eine Liste anderer kompatibler Memoization-Bibliotheken:



Der Backport kann jetzt hier gefunden werden: pypi.python.org/pypi/backports.functools_lru_cache
Frederick Nord

@gerrit funktioniert theoretisch im Allgemeinen für Hash-Objekte - obwohl einige Hash-Objekte nur dann gleich sind, wenn sie dasselbe Objekt sind (wie benutzerdefinierte Objekte ohne explizite __hash __ () -Funktion).
Jonathan

1
@ Jonathan Es funktioniert, aber falsch. Wenn ich ein hashbares, veränderbares Argument übergebe und den Wert des Objekts nach dem ersten Aufruf der Funktion ändere, gibt der zweite Aufruf das geänderte und nicht das ursprüngliche Objekt zurück. Das ist mit ziemlicher Sicherheit nicht das, was der Benutzer will. Damit es für veränderbare Argumente funktioniert lru_cache, muss eine Kopie des Caching-Ergebnisses erstellt werden, und in der functools.lru_cacheImplementierung wird keine solche Kopie erstellt . Andernfalls besteht die Gefahr, dass beim Zwischenspeichern eines großen Objekts schwer zu findende Speicherprobleme auftreten.
Gerrit

@gerrit Würde es Ihnen etwas ausmachen, hier nachzufolgen : stackoverflow.com/questions/44583381/… ? Ich bin Ihrem Beispiel nicht ganz gefolgt.
Jonathan

28

Es hört sich so an, als würden Sie nicht nach einem universellen Memoization Decorator fragen (dh Sie interessieren sich nicht für den allgemeinen Fall, in dem Sie Rückgabewerte für verschiedene Argumentwerte zwischenspeichern möchten). Das heißt, Sie möchten dies haben:

x = obj.name  # expensive
y = obj.name  # cheap

Ein Allzweck-Memoisierungsdekorateur würde Ihnen Folgendes geben:

x = obj.name()  # expensive
y = obj.name()  # cheap

Ich behaupte, dass die Methodenaufrufsyntax einen besseren Stil hat, da sie die Möglichkeit einer teuren Berechnung nahe legt, während die Eigenschaftssyntax eine schnelle Suche vorschlägt.

[Update: Der klassenbasierte Memoization Decorator, mit dem ich zuvor verlinkt und hier zitiert habe, funktioniert nicht für Methoden. Ich habe es durch eine Dekorationsfunktion ersetzt.] Wenn Sie bereit sind, einen Allzweck-Memoisierungsdekorator zu verwenden, ist hier eine einfache:

def memoize(function):
  memo = {}
  def wrapper(*args):
    if args in memo:
      return memo[args]
    else:
      rv = function(*args)
      memo[args] = rv
      return rv
  return wrapper

Anwendungsbeispiel:

@memoize
def fibonacci(n):
  if n < 2: return n
  return fibonacci(n - 1) + fibonacci(n - 2)

Ein weiterer Memoization Decorator mit einer Begrenzung der Cache-Größe finden Sie hier .


Keiner der in allen Antworten genannten Dekorateure arbeitet für Methoden! Wahrscheinlich, weil sie klassenbasiert sind. Nur ein Selbst ist bestanden? Andere funktionieren gut, aber es ist schwierig, Werte in Funktionen zu speichern.
Tobias

2
Ich denke, Sie könnten auf ein Problem stoßen, wenn args nicht hashbar ist.
Unbekannt

1
@ Unbekannt Ja, der erste Dekorateur, den ich hier zitiert habe, ist auf Hash-Typen beschränkt. Der bei ActiveState (mit der Cache-Größenbeschränkung) fasst die Argumente in eine (hashable) Zeichenfolge zusammen, die natürlich teurer, aber allgemeiner ist.
Nathan Kitchen

@vanity Vielen Dank, dass Sie auf die Einschränkungen der klassenbasierten Dekorateure hingewiesen haben. Ich habe meine Antwort überarbeitet, um eine Dekorationsfunktion anzuzeigen, die für Methoden funktioniert (ich habe diese tatsächlich getestet).
Nathan Kitchen

1
@SiminJie Der Dekorator wird nur einmal aufgerufen, und die zurückgegebene Funktion ist dieselbe, die für alle verschiedenen Aufrufe von verwendet wird fibonacci. Diese Funktion verwendet immer das gleiche memoWörterbuch.
Nathan Kitchen

22
class memorize(dict):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args):
        return self[args]

    def __missing__(self, key):
        result = self[key] = self.func(*key)
        return result

Beispielverwendungen:

>>> @memorize
... def foo(a, b):
...     return a * b
>>> foo(2, 4)
8
>>> foo
{(2, 4): 8}
>>> foo('hi', 3)
'hihihi'
>>> foo
{(2, 4): 8, ('hi', 3): 'hihihi'}

Seltsam! Wie funktioniert das? Es scheint nicht wie andere Dekorateure, die ich gesehen habe.
PascalVKooten

1
Diese Lösung gibt einen TypeError zurück, wenn man Schlüsselwortargumente verwendet, z. B. foo (3, b = 5)
kadee

1
Das Problem der Lösung ist, dass es kein Speicherlimit gibt. Die genannten Argumente können einfach zu __ call__ und __ missing__ hinzugefügt werden, wie ** nargs
Leonid Mednikov

16

Python 3.8 functools.cached_propertyDekorateur

https://docs.python.org/dev/library/functools.html#functools.cached_property

cached_propertyvon Werkzeug wurde erwähnt unter: https://stackoverflow.com/a/5295190/895245 aber eine angeblich abgeleitete Version wird in 3.8 zusammengeführt, was großartig ist.

Dieser Dekorateur kann als Caching @propertyoder als Reiniger angesehen werden @functools.lru_cache wenn Sie keine Argumente haben.

Die Dokumente sagen:

@functools.cached_property(func)

Transformieren Sie eine Methode einer Klasse in eine Eigenschaft, deren Wert einmal berechnet und dann als normales Attribut für die Lebensdauer der Instanz zwischengespeichert wird. Ähnlich wie bei property (), mit dem Zusatz von Caching. Nützlich für teure berechnete Eigenschaften von Instanzen, die ansonsten effektiv unveränderlich sind.

Beispiel:

class DataSet:
    def __init__(self, sequence_of_numbers):
        self._data = sequence_of_numbers

    @cached_property
    def stdev(self):
        return statistics.stdev(self._data)

    @cached_property
    def variance(self):
        return statistics.variance(self._data)

Neu in Version 3.8.

Hinweis Dieser Dekorator erfordert, dass das dict- Attribut auf jeder Instanz eine veränderbare Zuordnung ist. Dies bedeutet, dass es mit einigen Typen nicht funktioniert, z. B. mit Metaklassen (da die dict- Attribute auf Typinstanzen schreibgeschützte Proxys für den Klassennamensraum sind) und solchen, die Slots angeben, ohne dict als einen der definierten Slots (als solche Klassen) einzuschließen kein Diktatattribut angeben ).



9

Ich habe diese einfache Dekorationsklasse codiert, um Funktionsantworten zwischenzuspeichern. Ich finde es SEHR nützlich für meine Projekte:

from datetime import datetime, timedelta 

class cached(object):
    def __init__(self, *args, **kwargs):
        self.cached_function_responses = {}
        self.default_max_age = kwargs.get("default_cache_max_age", timedelta(seconds=0))

    def __call__(self, func):
        def inner(*args, **kwargs):
            max_age = kwargs.get('max_age', self.default_max_age)
            if not max_age or func not in self.cached_function_responses or (datetime.now() - self.cached_function_responses[func]['fetch_time'] > max_age):
                if 'max_age' in kwargs: del kwargs['max_age']
                res = func(*args, **kwargs)
                self.cached_function_responses[func] = {'data': res, 'fetch_time': datetime.now()}
            return self.cached_function_responses[func]['data']
        return inner

Die Verwendung ist unkompliziert:

import time

@cached
def myfunc(a):
    print "in func"
    return (a, datetime.now())

@cached(default_max_age = timedelta(seconds=6))
def cacheable_test(a):
    print "in cacheable test: "
    return (a, datetime.now())


print cacheable_test(1,max_age=timedelta(seconds=5))
print cacheable_test(2,max_age=timedelta(seconds=5))
time.sleep(7)
print cacheable_test(3,max_age=timedelta(seconds=5))

1
Ihre erste @cachedfehlt Klammer. Andernfalls wird das cachedObjekt nur anstelle von zurückgegeben, myfuncund wenn es aufgerufen myfunc()wird, innerwird es immer als Rückgabewert zurückgegeben
Markus Meskanen

6

HAFTUNGSAUSSCHLUSS: Ich bin der Autor von kids.cache .

Sie sollten überprüfen kids.cache, es bietet einen @cacheDekorator, der auf Python 2 und Python 3 funktioniert. Keine Abhängigkeiten, ~ 100 Codezeilen. Es ist sehr einfach zu verwenden, zum Beispiel mit Blick auf Ihren Code, Sie könnten es so verwenden:

pip install kids.cache

Dann

from kids.cache import cache
...
class MyClass(object):
    ...
    @cache            # <-- That's all you need to do
    @property
    def name(self):
        return 1 + 1  # supposedly expensive calculation

Oder Sie könnten den @cacheDekorateur nach dem setzen@property (gleichen Ergebnis) setzen.

Die Verwendung des Cache für eine Eigenschaft wird als verzögerte Auswertung bezeichnet und kids.cachekann viel mehr (sie funktioniert für Funktionen mit beliebigen Argumenten, Eigenschaften, Methoden aller Art und sogar Klassen ...). Für fortgeschrittene Benutzer kids.cacheUnterstützung, cachetoolsdie Python 2 und Python 3 (LRU-, LFU-, TTL-, RR-Cache) mit ausgefallenen Cache-Speichern versorgt.

WICHTIGER HINWEIS : Der Standard-Cache-Speicher von kids.cacheist ein Standard-Diktat, das nicht für Programme mit langer Laufzeit und immer unterschiedlichen Abfragen empfohlen wird, da dies zu einem ständig wachsenden Caching-Speicher führen würde. Für diese Verwendung können Sie beispielsweise andere Cache-Speicher einbinden ( @cache(use=cachetools.LRUCache(maxsize=2))um Ihre Funktion / Eigenschaft / Klasse / Methode zu dekorieren ...).


Dieses Modul scheint bei Python 2 ~ 0,9 s zu einer langsamen Importzeit zu führen (siehe: pastebin.com/raw/aA1ZBE9Z ). Ich vermute, dass dies an dieser Zeile github.com/0k/kids.cache/blob/master/src/kids/__init__.py#L3 liegt (siehe setuptools-Einstiegspunkte). Ich erstelle ein Problem dafür.
Att Righ

Hier ist ein Problem mit dem oben genannten github.com/0k/kids.cache/issues/9 .
Att Righ

Dies würde zu einem Speicherverlust führen.
Timothy Zhang

@vaab erstellt eine Instanz cvon MyClassund überprüft sie mit objgraph.show_backrefs([c], max_depth=10)einer Ref-Kette vom Klassenobjekt MyClasszu c. Das heißt, cwürde nie veröffentlicht werden, bis die MyClassveröffentlicht wurden.
Timothy Zhang

@TimothyZhang Sie sind eingeladen und willkommen, Ihre Bedenken in github.com/0k/kids.cache/issues/10 hinzuzufügen . Stackoverflow ist nicht der richtige Ort, um eine angemessene Diskussion darüber zu führen. Weitere Klarstellungen sind erforderlich. Danke für deine Rückmeldung.
Vaab


4

Es gibt Fastcache , dh "C-Implementierung von Python 3 functools.lru_cache. Bietet eine 10-30-fache Beschleunigung gegenüber der Standardbibliothek."

Gleich wie gewählte Antwort , nur anderer Import:

from fastcache import lru_cache
@lru_cache(maxsize=128, typed=False)
def f(a, b):
    pass

Außerdem wird es in Anaconda installiert, im Gegensatz zu functools, die installiert werden müssen .


1
functoolsist Teil der Standardbibliothek, der Link, den Sie gepostet haben, führt zu einer zufälligen Git-Gabel oder etwas anderem ...
cz


3

Wenn Sie Django Framework verwenden, verfügt es über eine solche Eigenschaft, um eine Ansicht oder Antwort der verwendeten APIs zwischenzuspeichern, @cache_page(time)und es kann auch andere Optionen geben.

Beispiel:

@cache_page(60 * 15, cache="special_cache")
def my_view(request):
    ...

Weitere Details finden Sie hier .


2

Zusammen mit dem Memoize-Beispiel habe ich die folgenden Python-Pakete gefunden:

  • Cachepy ; Es ermöglicht das Einrichten von ttl und \ oder der Anzahl der Aufrufe für zwischengespeicherte Funktionen. Man kann auch einen verschlüsselten dateibasierten Cache verwenden ...
  • Percache

1

Ich habe so etwas implementiert, indem ich pickle für die Persistenz und sha1 für kurze, mit ziemlicher Sicherheit eindeutige IDs verwendet habe. Grundsätzlich hat der Cache den Code der Funktion und die Historie der Argumente gehasht, um eine sha1 zu erhalten, und dann nach einer Datei mit dieser sha1 im Namen gesucht. Wenn es existierte, öffnete es es und gab das Ergebnis zurück; Wenn nicht, ruft es die Funktion auf und speichert das Ergebnis (optional nur, wenn die Verarbeitung eine gewisse Zeit in Anspruch genommen hat).

Trotzdem würde ich schwören, dass ich ein vorhandenes Modul gefunden habe, das dies getan hat, und mich hier befinde, um dieses Modul zu finden ... Das nächste, das ich finden kann, ist das, das ungefähr richtig aussieht: http: //chase-seibert.github. io / blog / 2011/11/23 / pythondjango-disk-based-caching-decorator.html

Das einzige Problem, das ich dabei sehe, ist, dass es für große Eingaben nicht gut funktioniert, da es str (arg) hasht, was für riesige Arrays nicht eindeutig ist.

Es wäre schön, wenn es ein unique_hash () -Protokoll gäbe , bei dem eine Klasse einen sicheren Hash ihres Inhalts zurückgibt . Ich habe das für die Typen, die mir wichtig waren, im Grunde manuell implementiert.




1

@lru_cache ist mit Standardfunktionswerten nicht perfekt

mein memDekorateur:

import inspect


def get_default_args(f):
    signature = inspect.signature(f)
    return {
        k: v.default
        for k, v in signature.parameters.items()
        if v.default is not inspect.Parameter.empty
    }


def full_kwargs(f, kwargs):
    res = dict(get_default_args(f))
    res.update(kwargs)
    return res


def mem(func):
    cache = dict()

    def wrapper(*args, **kwargs):
        kwargs = full_kwargs(func, kwargs)
        key = list(args)
        key.extend(kwargs.values())
        key = hash(tuple(key))
        if key in cache:
            return cache[key]
        else:
            res = func(*args, **kwargs)
            cache[key] = res
            return res
    return wrapper

und Code zum Testen:

from time import sleep


@mem
def count(a, *x, z=10):
    sleep(2)
    x = list(x)
    x.append(z)
    x.append(a)
    return sum(x)


def main():
    print(count(1,2,3,4,5))
    print(count(1,2,3,4,5))
    print(count(1,2,3,4,5, z=6))
    print(count(1,2,3,4,5, z=6))
    print(count(1))
    print(count(1, z=10))


if __name__ == '__main__':
    main()

Ergebnis - nur 3 mal mit Schlaf

aber @lru_cachedamit wird es 4 mal sein, denn das:

print(count(1))
print(count(1, z=10))

wird zweimal berechnet (schlechtes Arbeiten mit Standardeinstellungen)

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.