¿Hay un decorador para simplemente almacenar en caché los valores de retorno de la función?


157

Considera lo siguiente:

@property
def name(self):

    if not hasattr(self, '_name'):

        # expensive calculation
        self._name = 1 + 1

    return self._name

Soy nuevo, pero creo que el almacenamiento en caché se puede descomponer en un decorador. Solo que no encontré uno igual;)

PD: el cálculo real no depende de valores mutables


Puede haber un decorador que tenga alguna capacidad como esa, pero no ha especificado a fondo lo que quiere. ¿Qué tipo de backend de caché estás usando? ¿Y cómo se tecleará el valor? Supongo por su código que lo que realmente está pidiendo es una propiedad de solo lectura en caché.
David Berger

Hay decoradores de memoria que realizan lo que llama "almacenamiento en caché"; Por lo general, trabajan en funciones como tales (ya sea que se conviertan en métodos o no) cuyos resultados dependen de sus argumentos (¡no en cosas mutables como self!) y, por lo tanto, mantienen un memorando separado.
Alex Martelli

Respuestas:


206

A partir de Python 3.2 hay un decorador incorporado:

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

Decorador para envolver una función con una memoria invocable que ahorra hasta el máximo de llamadas más recientes. Puede ahorrar tiempo cuando una función costosa o unida a E / S se llama periódicamente con los mismos argumentos.

Ejemplo de un caché LRU para calcular números de 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)

Si está atascado con Python 2.x, aquí hay una lista de otras bibliotecas de memorización compatibles:



el backport ahora se puede encontrar aquí: pypi.python.org/pypi/backports.functools_lru_cache
Frederick Nord

@gerrit en teoría funciona para los objetos que se pueden compartir en general, aunque algunos objetos que se pueden compartir solo son iguales si son el mismo objeto (como los objetos definidos por el usuario sin una función explícita __hash __ ()).
Jonathan

1
@ Jonathan Funciona, pero erróneamente. Si paso un argumento mutable y hashable, y cambio el valor del objeto después de la primera llamada de la función, la segunda llamada devolverá el objeto cambiado, no el original. Es casi seguro que no es lo que el usuario quiere. Para que funcione para argumentos mutables se requeriría lru_cachehacer una copia de cualquier resultado que esté almacenando en caché, y no se está realizando dicha copia en la functools.lru_cacheimplementación. Si lo hace, también correría el riesgo de crear problemas de memoria difíciles de encontrar cuando se usa para almacenar en caché un objeto grande.
gerrit

@gerrit ¿Le importaría seguir aquí: stackoverflow.com/questions/44583381/… ? No seguí completamente tu ejemplo.
Jonathan

28

Parece que no está pidiendo un decorador de memorización de propósito general (es decir, no está interesado en el caso general en el que desea almacenar en caché los valores de retorno para diferentes valores de argumento). Es decir, te gustaría tener esto:

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

mientras que un decorador de memorización de uso general le daría esto:

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

Sostengo que la sintaxis de llamada al método es mejor estilo, porque sugiere la posibilidad de un cálculo costoso, mientras que la sintaxis de propiedad sugiere una búsqueda rápida.

[Actualización: el decorador de memoria basado en clases al que me había vinculado y citado aquí anteriormente no funciona para los métodos. Lo he reemplazado con una función de decorador.] Si está dispuesto a usar un decorador de memorización de propósito general, aquí hay uno simple:

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

Ejemplo de uso:

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

Otra decorador memoization con un límite en el tamaño de caché se puede encontrar aquí .


¡Ninguno de los decoradores mencionados en todas las respuestas funciona para los métodos! Probablemente porque están basados ​​en clases. ¿Solo se pasa un yo? Otros funcionan bien, pero es crucial almacenar valores en funciones.
Tobias

2
Creo que puede encontrarse con un problema si args no es hashaable.
Desconocido

1
@ Desconocido Sí, el primer decorador que he citado aquí se limita a los tipos hashable. El que está en ActiveState (con el límite de tamaño de caché) conserva los argumentos en una cadena (hashable) que, por supuesto, es más costosa pero más general.
Nathan Kitchen

@vanity Gracias por señalar las limitaciones de los decoradores basados ​​en clases. Revisé mi respuesta para mostrar una función de decorador, que funciona para métodos (en realidad probé esta).
Nathan Kitchen

1
@SiminJie El decorador solo se llama una vez, y la función envuelta que devuelve es la misma que se utiliza para todas las llamadas diferentes fibonacci. Esa función siempre usa el mismo memodiccionario.
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

Usos de muestra:

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

¡Extraño! ¿Como funciona esto? No se parece a otros decoradores que he visto.
PascalVKooten

1
Esta solución devuelve un error de tipo si se usan argumentos de palabras clave, por ejemplo, foo (3, b = 5)
kadee

1
El problema de la solución es que no tiene un límite de memoria. En cuanto a los argumentos nombrados, puede agregarlos a __ call__ y __ missing__ como ** nargs
Leonid Mednikov el

16

functools.cached_propertyDecorador Python 3.8

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

cached_propertyde Werkzeug se mencionó en: https://stackoverflow.com/a/5295190/895245 pero una versión supuestamente derivada se fusionará con 3.8, lo cual es increíble.

Este decorador puede verse como almacenamiento en caché @property, o como un limpiador @functools.lru_cachepara cuando no tienes ningún argumento.

Los documentos dicen:

@functools.cached_property(func)

Transforme un método de una clase en una propiedad cuyo valor se calcula una vez y luego se almacena en caché como un atributo normal para la vida de la instancia. Similar a la propiedad (), con la adición de almacenamiento en caché. Útil para propiedades computacionales costosas de instancias que de otra manera son efectivamente inmutables

Ejemplo:

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)

Nuevo en la versión 3.8.

Nota Este decorador requiere que el atributo dict en cada instancia sea una asignación mutable. Esto significa que no funcionará con algunos tipos, como las metaclases (ya que los atributos dict en instancias de tipo son proxies de solo lectura para el espacio de nombres de clase) y aquellos que especifican slots sin incluir dict como uno de los slots definidos (como tales clases no proporcione un atributo dict en absoluto).



9

Codifiqué esta clase de decorador simple para almacenar en caché las respuestas de la función. Lo encuentro MUY útil para mis proyectos:

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

El uso es sencillo:

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
Al primero @cachedle faltan paréntesis. De lo contrario sólo se devolverá el cachedobjeto en lugar de myfuncy cuando se le llama como myfunc()a continuación innersiempre que se obtiene es un valor de retorno
Markus Meskanen

6

DESCARGO DE RESPONSABILIDAD: Soy el autor de kids.cache .

Debe verificar kids.cache, proporciona un @cachedecorador que funciona en python 2 y python 3. Sin dependencias, ~ 100 líneas de código. Es muy sencillo de usar, por ejemplo, teniendo en cuenta su código, puede usarlo así:

pip install kids.cache

Luego

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

O podría poner el @cachedecorador después del @property(mismo resultado).

Usar caché en una propiedad se llama evaluación perezosa , kids.cachepuede hacer mucho más (funciona en función con cualquier argumento, propiedades, cualquier tipo de método e incluso clases ...). Para usuarios avanzados, kids.cachesoporte cachetoolsque proporciona almacenes de caché elegantes para python 2 y python 3 (caché LRU, LFU, TTL, RR).

NOTA IMPORTANTE : el almacén de caché predeterminado kids.cachees un dict estándar, que no se recomienda para programas de larga ejecución con consultas siempre diferentes, ya que conduciría a un almacén de almacenamiento en caché cada vez mayor. Para este uso, puede agregar otros almacenes de caché, por ejemplo ( @cache(use=cachetools.LRUCache(maxsize=2))para decorar su función / propiedad / clase / método ...)


Este módulo parece resultar en un tiempo de importación lento en python 2 ~ 0.9s (ver: pastebin.com/raw/aA1ZBE9Z ). Sospecho que esto se debe a esta línea github.com/0k/kids.cache/blob/master/src/kids/__init__.py#L3 (cf puntos de entrada de setuptools). Estoy creando un problema para esto.
Att Righ

Aquí hay un problema para el github.com/0k/kids.cache/issues/9 anterior .
Att Righ

Esto llevaría a una pérdida de memoria.
Timothy Zhang

@vaab crear una instancia cde MyClass, e inspeccionar con objgraph.show_backrefs([c], max_depth=10), hay una cadena ref de la clase de objeto MyClassa c. Es decir, cnunca se lanzaría hasta que se MyClasshaya lanzado.
Timothy Zhang

@TimothyZhang está invitado y bienvenido a agregar sus inquietudes en github.com/0k/kids.cache/issues/10 . Stackoverflow no es el lugar adecuado para tener una discusión adecuada al respecto. Y se necesitan más aclaraciones. Gracias por tus comentarios.
vaab


4

Hay fastcache , que es "Implementación en C de Python 3 functools.lru_cache. Proporciona una velocidad de 10-30x sobre la biblioteca estándar".

Igual que la respuesta elegida , solo diferente importación:

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

Además, viene instalado en Anaconda , a diferencia de las functools que deben instalarse .


1
functoolses parte de la biblioteca estándar, el enlace que ha publicado es un tenedor aleatorio de git o algo más ...
cz


3

Si está utilizando Django Framework, tiene dicha propiedad para almacenar en caché una vista o respuesta de API utilizando @cache_page(time) y también puede haber otras opciones.

Ejemplo:

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

Más detalles se pueden encontrar aquí .


2

Junto con el ejemplo de Memoize encontré los siguientes paquetes de python:

  • cachepy ; Permite configurar ttl y \ o el número de llamadas para funciones en caché; Además, uno puede usar caché basada en archivos cifrados ...
  • percaché

1

Implementé algo como esto, usando pickle para persistencia y usando sha1 para identificaciones cortas casi ciertamente únicas. Básicamente, la memoria caché codificó el código de la función y el historial de argumentos para obtener un sha1 y luego buscó un archivo con ese sha1 en el nombre. Si existió, lo abrió y devolvió el resultado; si no, llama a la función y guarda el resultado (opcionalmente solo guarda si se procesó una cierta cantidad de tiempo).

Dicho esto, juro que encontré un módulo existente que hizo esto y me encontré aquí tratando de encontrar ese módulo ... Lo más cerca que puedo encontrar es esto, que se ve a la derecha: http: //chase-seibert.github. io / blog / 2011/11/23 / pythondjango-disk-based-caching-decorator.html

El único problema que veo con eso es que no funcionaría bien para entradas grandes, ya que tiene hash str (arg), que no es exclusivo para matrices gigantes.

Sería bueno si hubiera un protocolo unique_hash () que hiciera que una clase devolviera un hash seguro de su contenido. Básicamente lo implementé manualmente para los tipos que me importaban.



1

Si está utilizando Django y desea almacenar en caché las vistas, consulte la respuesta de Nikhil Kumar .


Pero si desea almacenar en caché los resultados de CUALQUIER función, puede usar django-cache-utils .

Reutiliza cachés de Django y proporciona un cacheddecorador fácil de usar :

from cache_utils.decorators import cached

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

1

@lru_cache no es perfecto con valores de función predeterminados

mi memdecorador:

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

y código para probar:

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

resultado: solo 3 veces con el sueño

pero con @lru_cacheeso será 4 veces, porque esto:

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

se calculará dos veces (mal funcionamiento con valores predeterminados)

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.