Czy istnieje dekorator, który po prostu buforuje wartości zwracane przez funkcję?


157

Rozważ następujące:

@property
def name(self):

    if not hasattr(self, '_name'):

        # expensive calculation
        self._name = 1 + 1

    return self._name

Jestem nowy, ale myślę, że buforowanie można rozłożyć na dekorator. Tylko że takiego nie znalazłem;)

PS rzeczywiste obliczenia nie zależą od zmiennych wartości


Może istnieć dekorator, który ma takie możliwości, ale nie określiłeś dokładnie, czego chcesz. Jakiego rodzaju zaplecza buforowania używasz? W jaki sposób wartość zostanie wpisana? Zakładam na podstawie twojego kodu, że to, o co naprawdę prosisz, to buforowana właściwość tylko do odczytu.
David Berger

Istnieją dekoratory zapamiętujące, które wykonują to, co nazywasz „buforowaniem”; zazwyczaj pracują nad funkcjami jako takimi (niezależnie od tego, czy mają stać się metodami, czy nie), których wyniki zależą od ich argumentów (a nie od zmiennych elementów, takich jak self! -), a więc zachowują oddzielny dykt memo.
Alex Martelli

Odpowiedzi:


206

Począwszy od Pythona 3.2 jest wbudowany dekorator:

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

Dekorator, aby otoczyć funkcję funkcją zapamiętywania, która zapisuje do maksymalnego rozmiaru ostatnich połączeń. Może to zaoszczędzić czas, gdy droga lub związana z We / Wy funkcja jest okresowo wywoływana z tymi samymi argumentami.

Przykład pamięci podręcznej LRU do obliczania liczb Fibonacciego :

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

Jeśli utkniesz z Pythonem 2.x, oto lista innych kompatybilnych bibliotek zapamiętywania:




@gerrit w teorii działa ogólnie dla obiektów z możliwością mieszania - chociaż niektóre obiekty z możliwością mieszania są równe tylko wtedy, gdy są tym samym obiektem (jak obiekty zdefiniowane przez użytkownika bez jawnej funkcji __hash __ ()).
Jonathan

1
@Jonathan Działa, ale źle. Jeśli przekażę argument haszowalny, zmienny i zmienię wartość obiektu po pierwszym wywołaniu funkcji, drugie wywołanie zwróci zmieniony, a nie oryginalny obiekt. To prawie na pewno nie to, czego chce użytkownik. Aby to działało dla zmiennych argumentów, wymagałoby lru_cachewykonania kopii dowolnego wyniku, który jest zapisywany w pamięci podręcznej, a taka kopia nie jest tworzona w functools.lru_cacheimplementacji. Takie postępowanie groziłoby również stworzeniem trudnych do znalezienia problemów z pamięcią, gdy jest używane do buforowania dużego obiektu.
gerrit

@gerrit Czy mógłbyś śledzić tutaj: stackoverflow.com/questions/44583381/… ? Nie poszedłem całkowicie za twoim przykładem.
Jonathan

28

Wygląda na to, że nie prosisz o dekorator zapamiętywania ogólnego przeznaczenia (tj. Nie interesuje Cię ogólny przypadek, w którym chcesz buforować wartości zwracane dla różnych wartości argumentów). Oznacza to, że chciałbyś mieć to:

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

podczas gdy dekorator zapamiętywania ogólnego przeznaczenia dałby ci to:

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

Twierdzę, że składnia wywołania metody jest lepsza, ponieważ sugeruje możliwość kosztownych obliczeń, podczas gdy składnia właściwości sugeruje szybkie sprawdzenie.

[Aktualizacja: Oparty na klasach dekorator zapamiętywania, z którym łączyłem się i cytowałem tutaj wcześniej, nie działa dla metod. Zastąpiłem go funkcją dekoratora.] Jeśli chcesz użyć dekoratora do zapamiętywania ogólnego przeznaczenia, oto prosty:

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

Przykładowe użycie:

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

Kolejny dekorator zapamiętywania z ograniczeniem rozmiaru pamięci podręcznej można znaleźć tutaj .


Żaden z dekoratorów wymienionych we wszystkich odpowiedziach nie sprawdza się w przypadku metod! Prawdopodobnie dlatego, że są oparte na klasach. Przekazywane jest tylko jedno ja? Inne działają dobrze, ale przechowywanie wartości w funkcjach jest okrucieństwem.
Tobias

2
Myślę, że możesz napotkać problem, jeśli argumenty nie są haszowane.
Nieznany

1
@Nieznany Tak, pierwszy dekorator, który tutaj zacytowałem, jest ograniczony do typów z funkcją mieszania. Ten w ActiveState (z limitem rozmiaru pamięci podręcznej) zbiera argumenty w (haszowalny) ciąg, który jest oczywiście droższy, ale bardziej ogólny.
Nathan Kitchen

@vanity Dzięki za wskazanie ograniczeń dekoratorów opartych na klasach. Poprawiłem swoją odpowiedź, aby pokazać funkcję dekoratora, która działa dla metod (faktycznie przetestowałem tę).
Nathan Kitchen

1
@SiminJie Dekorator jest wywoływany tylko raz, a opakowana funkcja, którą zwraca, jest taka sama, jak używana dla wszystkich różnych wywołań fibonacci. Ta funkcja zawsze używa tego samego memosłownika.
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

Przykładowe zastosowania:

>>> @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'}

Dziwne! Jak to działa? Nie wygląda to na innych dekoratorów, których widziałem.
PascalVKooten

1
To rozwiązanie zwraca TypeError, jeśli używa się argumentów słów kluczowych, np. Foo (3, b = 5)
kadee

1
Problem rozwiązania polega na tym, że nie ma limitu pamięci. Jeśli chodzi o wymienione argumenty, możesz po prostu dodać je do __ call__ i __ missing__, jak ** nargs
Leonid Mednikov

16

functools.cached_propertyDekorator Pythona 3.8

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

cached_propertyz Werkzeug wspomniano na: https://stackoverflow.com/a/5295190/895245, ale podobno pochodna wersja zostanie scalona z wersją 3.8, co jest niesamowite.

Ten dekorator może być postrzegany jako buforowanie @propertylub jako środek czyszczący, @functools.lru_cachegdy nie masz żadnych argumentów.

Doktorzy mówią:

@functools.cached_property(func)

Przekształć metodę klasy w właściwość, której wartość jest obliczana raz, a następnie zapisywana w pamięci podręcznej jako normalny atrybut przez cały okres istnienia instancji. Podobnie jak property (), z dodatkiem buforowania. Przydatne w przypadku kosztownych obliczonych właściwości wystąpień, które w przeciwnym razie są skutecznie niezmienne.

Przykład:

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)

Nowość w wersji 3.8.

Uwaga Ten dekorator wymaga, aby atrybut dict w każdej instancji był zmiennym mapowaniem. Oznacza to, że nie będzie działać z niektórymi typami, takimi jak metaklasy (ponieważ atrybuty dict w instancjach typu są serwerami proxy tylko do odczytu dla przestrzeni nazw klas) oraz z tymi, które określają gniazda bez uwzględniania dict jako jednego ze zdefiniowanych gniazd (jako takie klasy w ogóle nie podawaj atrybutu dict ).



9

Zakodowałem tę prostą klasę dekoratora, aby buforowała odpowiedzi funkcji. Uważam, że jest to BARDZO przydatne w moich projektach:

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

Użycie jest proste:

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
Twoim pierwszym @cachedbrakuje nawiasu. W przeciwnym razie zwróci cachedobiekt tylko w miejsce myfunci po wywołaniu, tak jak myfunc()wtedy inner, zawsze zostanie zwrócony jako wartość zwracana
Markus Meskanen

6

ZRZECZENIE SIĘ: Jestem autorem kids.cache .

Powinieneś sprawdzić kids.cache, zawiera @cachedekorator, który działa na Pythonie 2 i Pythonie 3. Brak zależności, ~ 100 linii kodu. Jest to bardzo proste w użyciu, na przykład, mając na uwadze swój kod, możesz go użyć w następujący sposób:

pip install kids.cache

Następnie

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

Lub możesz umieścić @cachedekorator po @property(ten sam wynik).

Używanie pamięci podręcznej do właściwości nazywa się leniwym szacowaniem , kids.cachemoże zrobić znacznie więcej (działa na funkcji z dowolnymi argumentami, właściwościami, dowolnym typem metod, a nawet klasami ...). Dla zaawansowanych użytkowników kids.cacheobsługuje, cachetoolsktóre zapewnia fantazyjne magazyny pamięci podręcznej dla python 2 i python 3 (LRU, LFU, TTL, RR cache).

WAŻNA UWAGA : domyślnym magazynem pamięci podręcznej kids.cachejest standardowy dykt, który nie jest zalecany dla długo działających programów z ciągle różnymi zapytaniami, ponieważ prowadziłoby to do stale rosnącego magazynu buforowania. W tym celu możesz podłączyć inne sklepy z pamięcią podręczną, używając na przykład ( @cache(use=cachetools.LRUCache(maxsize=2))do dekoracji funkcji / właściwości / klasy / metody ...)


Wydaje się, że ten moduł powoduje długi czas importu w Pythonie 2 ~ 0.9s (patrz: pastebin.com/raw/aA1ZBE9Z ). Podejrzewam, że jest to spowodowane tą linią github.com/0k/kids.cache/blob/master/src/kids/__init__.py#L3 (por. Punkty wejścia setuptools). Tworzę problem z tym.
Att Righ

Oto problem dotyczący powyższego github.com/0k/kids.cache/issues/9 .
Att Righ

Prowadziłoby to do wycieku pamięci.
Timothy Zhang

@vaab utworzyć egzemplarz co MyClassi sprawdzić je objgraph.show_backrefs([c], max_depth=10), to łańcuch ref z klasą obiektu MyClassdo c. To znaczy, cnigdy nie zostałby zwolniony, dopóki nie MyClasszostał zwolniony.
Timothy Zhang

@TimothyZhang, zapraszamy i zapraszamy do zgłaszania swoich obaw na github.com/0k/kids.cache/issues/10 . Stackoverflow nie jest odpowiednim miejscem na dyskusję na ten temat. Potrzebne są dalsze wyjaśnienia. Dziękujemy za twoją opinię.
vaab


4

Jest fastcache , czyli „implementacja Pythona 3 functools.lru_cache w C. Zapewnia 10-30-krotne przyspieszenie w stosunku do standardowej biblioteki”.

Taka sama jak wybrana odpowiedź , tylko inny import:

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

Jest również instalowany w Anaconda , w przeciwieństwie do functools, które należy zainstalować .


1
functoolsjest częścią standardowej biblioteki, zamieszczony link prowadzi do losowego widelca git lub czegoś innego ...
cz


3

Jeśli używasz Django Framework, ma on taką właściwość, aby buforować widok lub odpowiedź interfejsu API, @cache_page(time)a także mogą istnieć inne opcje.

Przykład:

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

Więcej szczegółów można znaleźć tutaj .


2

Wraz z przykładem Memoize znalazłem następujące pakiety Pythona:

  • cachepy ; Pozwala ustawić ttl i \ lub liczbę wywołań funkcji w pamięci podręcznej; Można również użyć zaszyfrowanej pamięci podręcznej opartej na plikach ...
  • percache

1

Zaimplementowałem coś takiego, używając pickle dla wytrwałości i używając sha1 dla krótkich, prawie na pewno unikalnych identyfikatorów. Zasadniczo pamięć podręczna haszowała kod funkcji i hist argumentów, aby uzyskać sha1, a następnie szukał pliku z tym sha1 w nazwie. Jeśli istniał, otwierał go i zwracał wynik; jeśli nie, wywołuje funkcję i zapisuje wynik (opcjonalnie zapisuje tylko wtedy, gdy przetworzenie zajęło pewien czas).

To powiedziawszy, przysięgam, że znalazłem istniejący moduł, który to zrobił i znalazłem się tutaj, próbując znaleźć ten moduł ... Najbliższy, jaki mogę znaleźć, to ten, który wygląda dobrze: http: //chase-seibert.github. io / blog / 2011/11/23 / pythondjango-disk-based-caching-decorator.html

Jedynym problemem, jaki widzę z tym, jest to, że nie działałoby to dobrze w przypadku dużych danych wejściowych, ponieważ haszuje str (arg), co nie jest unikalne dla gigantycznych tablic.

Byłoby miło, gdyby istniał protokół unique_hash (), który miałby klasę zwracającą bezpieczny hash swojej zawartości. W zasadzie ręcznie zaimplementowałem to dla typów, na których mi zależało.



1

Jeśli używasz Django i chcesz buforować widoki, zobacz odpowiedź Nikhila Kumara .


Ale jeśli chcesz buforować DOWOLNE wyniki funkcji, możesz użyć django-cache-utils .

Ponownie wykorzystuje pamięci podręczne Django i zapewnia łatwy w użyciu cacheddekorator:

from cache_utils.decorators import cached

@cached(60)
def foo(x, y=0):
    print 'foo is called'
    return x+y

1

@lru_cache nie jest doskonały z domyślnymi wartościami funkcji

mój memdekorator:

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

i kod do testowania:

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

wynik - tylko 3 razy ze snem

ale z @lru_cachenim będzie 4 razy, bo to:

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

zostanie obliczony dwukrotnie (źle działa z domyślnymi)

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.