Esiste un decoratore che memorizza semplicemente nella cache i valori di ritorno della funzione?


157

Considera quanto segue:

@property
def name(self):

    if not hasattr(self, '_name'):

        # expensive calculation
        self._name = 1 + 1

    return self._name

Sono nuovo, ma penso che la memorizzazione nella cache possa essere presa in considerazione in un decoratore. Solo io non ne ho trovato uno simile;)

PS il calcolo reale non dipende da valori mutabili


Potrebbe esserci un decoratore là fuori che ha alcune capacità del genere, ma non hai specificato a fondo cosa vuoi. Che tipo di backend nella cache stai usando? E come verrà digitato il valore? Presumo dal tuo codice che ciò che stai veramente chiedendo sia una proprietà di sola lettura memorizzata nella cache.
David Berger,

Ci sono decoratori memoizing che eseguono ciò che tu chiami "cache"; in genere lavorano su funzioni in quanto tali (che siano o meno destinate a diventare metodi) i cui risultati dipendono dai loro argomenti (non da cose mutevoli come il sé! -) e quindi mantengono un memo-dict separato.
Alex Martelli,

Risposte:


206

A partire da Python 3.2 c'è un decoratore incorporato:

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

Decoratore per avvolgere una funzione con un calloble di memorizzazione che consente di salvare fino alle chiamate più recenti di dimensioni massime. Può risparmiare tempo quando una funzione costosa o associata I / O viene periodicamente chiamata con gli stessi argomenti.

Esempio di una cache LRU per il calcolo dei numeri di Fibonacci :

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

Se sei bloccato con Python 2.x, ecco un elenco di altre librerie di memoization compatibili:


1
Codice di backport.activestate.com/recipes/…
Kos,

il backport ora può essere trovato qui: pypi.python.org/pypi/backports.functools_lru_cache
Frederick Nord

@gerrit in teoria funziona per gli oggetti hash in generale - anche se alcuni oggetti hash sono uguali solo se sono lo stesso oggetto (come gli oggetti definiti dall'utente senza una funzione esplicita __hash __ ()).
Jonathan,

1
@Jonathan Funziona, ma a torto. Se passo un argomento modificabile e modificabile e cambio il valore dell'oggetto dopo la prima chiamata della funzione, la seconda chiamata restituirà l'oggetto modificato, non l'originale. Questo non è quasi certamente ciò che l'utente desidera. Perché funzioni per argomenti mutabili richiederebbe lru_cacheuna copia di qualsiasi risultato che sta memorizzando nella cache, e nessuna tale copia viene eseguita functools.lru_cachenell'implementazione. Ciò rischierebbe anche di creare problemi di memoria difficili da trovare se utilizzato per memorizzare nella cache un oggetto di grandi dimensioni.
Gerrit,

@gerrit Ti dispiacerebbe seguire qui: stackoverflow.com/questions/44583381/… ? Non ho seguito del tutto il tuo esempio.
Jonathan,

28

Sembra che tu non stia chiedendo un decoratore di memoization per scopi generici (ovvero, non sei interessato al caso generale in cui desideri memorizzare nella cache valori di ritorno per valori di argomenti diversi). Cioè, ti piacerebbe avere questo:

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

mentre un decoratore di promemoria per scopi generici ti darebbe questo:

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

Presumo che la sintassi del metodo di chiamata sia uno stile migliore, perché suggerisce la possibilità di un calcolo costoso mentre la sintassi della proprietà suggerisce una rapida ricerca.

[Aggiornamento: il decoratore di memoization basato sulla classe che avevo collegato e citato qui in precedenza non funziona con i metodi. L'ho sostituito con una funzione decoratore.] Se sei disposto a utilizzare un decoratore di memoization generico, eccone uno semplice:

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

Esempio di utilizzo:

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

Un altro decoratore di memoization con un limite alla dimensione della cache può essere trovato qui .


Nessuno dei decoratori menzionati in tutte le risposte funziona con metodi! Probabilmente perché sono di classe. È passato solo un sé? Altri funzionano bene, ma è difficile conservare i valori nelle funzioni.
Tobias,

2
Penso che potresti riscontrare un problema se args non è hash.
Sconosciuto

1
@Sconosciuta Sì, il primo decoratore che ho citato qui è limitato ai tipi hash. Quello di ActiveState (con il limite della dimensione della cache) sottrae gli argomenti in una stringa (cancellabile) che è ovviamente più costosa ma più generale.
Nathan Kitchen,

@vanity Grazie per aver sottolineato i limiti dei decoratori di classe. Ho rivisto la mia risposta per mostrare una funzione decoratore, che funziona con i metodi (in realtà ho provato questo).
Nathan Kitchen,

1
@SiminJie Il decoratore viene chiamato solo una volta e la funzione di avvolgimento che restituisce è la stessa utilizzata per tutte le diverse chiamate a fibonacci. Tale funzione utilizza sempre lo stesso memodizionario.
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

Esempi di utilizzo:

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

Strano! Come funziona? Non sembra come altri decoratori che ho visto.
PascalVKooten,

1
Questa soluzione restituisce un errore di tipo se si utilizzano argomenti di parole chiave, ad esempio foo (3, b = 5)
kadee

1
Il problema della soluzione è che non ha un limite di memoria. Per quanto riguarda gli argomenti nominati, puoi semplicemente aggiungerli a __ call__ e __ missing__ come ** nargs
Leonid Mednikov

16

functools.cached_propertyDecoratore Python 3.8

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

cached_propertyda Werkzeug è stato menzionato su: https://stackoverflow.com/a/5295190/895245 ma una versione presumibilmente derivata verrà unita in 3.8, il che è fantastico.

Questo decoratore può essere visto come cache @propertyo come pulitore @functools.lru_cacheper quando non hai argomenti.

I documenti dicono:

@functools.cached_property(func)

Trasforma un metodo di una classe in una proprietà il cui valore viene calcolato una volta e quindi memorizzato nella cache come un normale attributo per la durata dell'istanza. Simile a property (), con l'aggiunta della cache. Utile per costose proprietà calcolate di istanze che altrimenti sarebbero effettivamente immutabili.

Esempio:

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)

Novità nella versione 3.8.

Nota Questo decoratore richiede che l' attributo dict su ogni istanza sia una mappatura mutabile. Ciò significa che non funzionerà con alcuni tipi, ad esempio metaclass (poiché gli attributi dict nelle istanze di tipo sono proxy di sola lettura per lo spazio dei nomi di classe) e quelli che specificano gli slot senza includere dict come uno degli slot definiti (come tali classi non fornire affatto un attributo dict ).



9

Ho codificato questa semplice classe di decoratore per memorizzare nella cache le risposte alle funzioni. Lo trovo MOLTO utile per i miei progetti:

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

L'utilizzo è semplice:

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
La tua prima @cachedparentesi mancante. Altrimenti restituirà solo l' cachedoggetto al posto di myfunce quando viene chiamato come myfunc()allora innerverrà sempre restituito come valore di ritorno
Markus Meskanen,

6

DISCLAIMER: sono l'autore di kids.cache .

Dovresti controllare kids.cache, fornisce un @cachedecoratore che funziona su Python 2 e Python 3. Nessuna dipendenza, ~ 100 righe di codice. È molto semplice da usare, ad esempio, tenendo presente il tuo codice, puoi usarlo in questo modo:

pip install kids.cache

Poi

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

Oppure potresti mettere il @cachedecoratore dopo il@property stesso risultato.

L'uso della cache su una proprietà è chiamato valutazione pigra , kids.cachepuò fare molto di più (funziona sulla funzione con qualsiasi argomento, proprietà, qualsiasi tipo di metodo e persino classi ...). Per utenti esperti, kids.cachesupporti cachetoolsche forniscono archivi cache di fantasia a Python 2 e Python 3 (LRU, LFU, TTL, RR cache).

NOTA IMPORTANTE : l'archivio cache predefinito di kids.cacheè un dict standard, che non è raccomandato per programmi a esecuzione prolungata con query sempre diverse in quanto porterebbe a un archivio di cache sempre crescente. Per questo utilizzo puoi collegare altri archivi cache usando ad esempio ( @cache(use=cachetools.LRUCache(maxsize=2))per decorare la tua funzione / proprietà / classe / metodo ...)


Questo modulo sembra comportare un tempo di importazione lento su python 2 ~ 0.9s (vedi: pastebin.com/raw/aA1ZBE9Z ). Sospetto che ciò sia dovuto a questa riga github.com/0k/kids.cache/blob/master/src/kids/__init__.py#L3 (vedi punti di accesso setuptools). Sto creando un problema per questo.
Att Righ,

Ecco un problema per quanto sopra github.com/0k/kids.cache/issues/9 .
Att Righ,

Ciò comporterebbe una perdita di memoria.
Timothy Zhang,

@vaab crea un'istanza cdi MyClass, e la ispeziona con objgraph.show_backrefs([c], max_depth=10), c'è una catena ref dall'oggetto classe MyClassa c. Vale a dire, cnon sarebbe mai stato rilasciato fino a quando non è MyClassstato rilasciato.
Timothy Zhang,

@TimothyZhang sei invitato e benvenuto per aggiungere le tue preoccupazioni in github.com/0k/kids.cache/issues/10 . Stackoverflow non è il posto giusto per discuterne adeguatamente. E sono necessari ulteriori chiarimenti. Grazie per il tuo feedback.
vaab,


4

C'è fastcache , che è "implementazione C di Python 3 functools.lru_cache. Fornisce una velocità di 10-30x rispetto alla libreria standard."

Stessa risposta scelta , solo importazione diversa:

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

Inoltre, viene installato in Anaconda , a differenza dei functools che devono essere installati .


1
functoolsfa parte della libreria standard, il link che hai pubblicato è un git fork casuale o qualcos'altro ...
cz


3

Se stai usando Django Framework, ha una tale proprietà per memorizzare nella cache una vista o una risposta dell'uso dell'API @cache_page(time)e ci possono essere anche altre opzioni.

Esempio:

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

Maggiori dettagli possono essere trovati qui .


2

Insieme all'esempio Memoize ho trovato i seguenti pacchetti python:

  • cachepy ; Permette di impostare ttl e \ o il numero di chiamate per le funzioni memorizzate nella cache; Inoltre, è possibile utilizzare la cache basata su file crittografata ...
  • percache

1

Ho implementato qualcosa del genere, usando pickle per la persistenza e usando sha1 per brevi ID quasi certamente unici. Fondamentalmente la cache ha hashed il codice della funzione e l'hist di argomenti per ottenere uno sha1, quindi ha cercato un file con quello sha1 nel nome. Se esisteva, lo apriva e restituiva il risultato; in caso contrario, chiama la funzione e salva il risultato (facoltativamente salva solo se il processo ha richiesto un certo tempo).

Detto questo, giuro che ho trovato un modulo esistente che lo ha fatto e mi trovo qui cercando di trovare quel modulo ... Il più vicino che posso trovare è questo, che sembra giusto: http: //chase-seibert.github. io / blog / 2011/11/23 / pythondjango-disk-based-caching-decorator.html

L'unico problema che vedo con questo è che non funzionerebbe bene per input di grandi dimensioni poiché ha str str (arg), che non è unico per array giganti.

Sarebbe bello se ci fosse un protocollo unique_hash () con una classe che restituisce un hash sicuro del suo contenuto. Fondamentalmente l'ho implementato manualmente per i tipi a cui tenevo.



1

Se stai usando Django e vuoi memorizzare nella cache le visualizzazioni, vedi la risposta di Nikhil Kumar .


Ma se si desidera memorizzare nella cache QUALSIASI risultato della funzione, è possibile utilizzare django-cache-utils .

Riutilizza le cache di Django e offre un cacheddecoratore facile da usare :

from cache_utils.decorators import cached

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

1

@lru_cache non è perfetto con i valori di funzione predefiniti

il mio memdecoratore:

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

e codice per il test:

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

risultato - solo 3 volte con il sonno

ma con @lru_cacheesso sarà 4 volte, perché questo:

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

sarà calcolato due volte (cattivo funzionamento con i valori predefiniti)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.