python - resueltos - Decorador para un método de clase que almacena en caché el valor de retorno después del primer acceso
poo python 3 (5)
Creo que estás mejor con un descriptor personalizado, ya que este es exactamente el tipo de cosas para las que son los descriptores. Al igual que:
class CachedProperty:
def __init__(self, name, get_the_value):
self.name = name
self.get_the_value = get_the_value
def __get__(self, obj, typ):
name = self.name
while True:
try:
return getattr(obj, name)
except AttributeError:
get_the_value = self.get_the_value
try:
# get_the_value can be a string which is the name of an obj method
value = getattr(obj, get_the_value)()
except AttributeError:
# or it can be another external function
value = get_the_value()
setattr(obj, name, value)
continue
break
class Mine:
cached_property = CachedProperty("_cached_property ", get_cached_property_value)
# OR:
class Mine:
cached_property = CachedProperty("_cached_property", "get_cached_property_value")
def get_cached_property_value(self):
return "the_value"
EDITAR: por cierto, ni siquiera necesita un descriptor personalizado. Puede simplemente almacenar en caché el valor dentro de su función de propiedad. P.ej:
@property
def test(self):
while True:
try:
return self._test
except AttributeError:
self._test = get_initial_value()
Eso es todo al respecto.
Sin embargo, muchos lo considerarían como un abuso de la property
y como una forma inesperada de usarlo. Y lo inesperado generalmente significa que debes hacerlo de otra manera, más explícita. Un descriptor personalizado de CachedProperty
es muy explícito, por lo que por esa razón lo preferiría al enfoque de property
, aunque requiere más código.
Mi problema y por que
Estoy tratando de escribir un decorador para un método de clase, @cachedproperty
. Quiero que se comporte de modo que cuando se llame por primera vez al método, éste se reemplace con su valor de retorno. También quiero que se comporte como @property
para que no sea necesario @property
explícitamente. Básicamente, debería ser indistinguible de @property
excepto que es más rápido, ya que solo calcula el valor una vez y luego lo almacena. Mi idea es que esto no ralentizaría la __init__
instancias como lo haría __init__
en __init__
. Es por eso que quiero hacer esto.
Lo que intenté
Primero, intenté anular el método fget
de la property
, pero es de solo lectura.
A continuación, pensé que intentaría implementar un decorador que debe llamarse la primera vez, pero luego almacena en caché los valores. Este no es mi objetivo final de un decorador de tipo de propiedad que nunca necesita ser llamado, pero pensé que este sería un problema más simple de abordar primero. En otras palabras, esta es una solución que no funciona para un problema un poco más simple.
Lo intenté:
def cachedproperty(func):
""" Used on methods to convert them to methods that replace themselves
with their return value once they are called. """
def cache(*args):
self = args[0] # Reference to the class who owns the method
funcname = inspect.stack()[0][3] # Name of the function, so that it can be overridden.
setattr(self, funcname, func()) # Replace the function with its value
return func() # Return the result of the function
return cache
Sin embargo, esto no parece funcionar. He probado esto con:
>>> class Test:
... @cachedproperty
... def test(self):
... print "Execute"
... return "Return"
...
>>> Test.test
<unbound method Test.cache>
>>> Test.test()
pero me sale un error sobre cómo la clase no se pasó al método:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unbound method cache() must be called with Test instance as first argument (got nothing instead)
En este punto, yo y mi conocimiento limitado de los métodos profundos de Python estamos muy confundidos, y no tengo idea de dónde salió mal mi código o cómo solucionarlo. (Nunca he tratado de escribir un decorador antes)
La pregunta
¿Cómo puedo escribir un decorador que devolverá el resultado de llamar a un método de clase la primera vez que se acceda (como @property
hace @property
) y ser reemplazado por un valor almacenado en caché para todas las consultas posteriores?
Espero que esta pregunta no sea demasiado confusa, traté de explicarlo lo mejor que pude.
En primer lugar la Test
debe ser instanciada
test = Test()
Segundo, no hay necesidad de inspect
porque podemos obtener el nombre de la propiedad de func.__name__
Y tercero, devolvemos la property(cache)
para que Python haga toda la magia.
def cachedproperty(func):
" Used on methods to convert them to methods that replace themselves/
with their return value once they are called. "
def cache(*args):
self = args[0] # Reference to the class who owns the method
funcname = func.__name__
ret_value = func(self)
setattr(self, funcname, ret_value) # Replace the function with its value
return ret_value # Return the result of the function
return property(cache)
class Test:
@cachedproperty
def test(self):
print "Execute"
return "Return"
>>> test = Test()
>>> test.test
Execute
''Return''
>>> test.test
''Return''
>>>
""
La versión de Django de este decorador hace exactamente lo que usted describe y es simple, así que además de mi comentario, lo copiaré aquí:
class cached_property(object):
"""
Decorator that converts a method with a single self argument into a
property cached on the instance.
Optional ``name`` argument allows you to make cached properties of other
methods. (e.g. url = cached_property(get_absolute_url, name=''url'') )
"""
def __init__(self, func, name=None):
self.func = func
self.__doc__ = getattr(func, ''__doc__'')
self.name = name or func.__name__
def __get__(self, instance, type=None):
if instance is None:
return self
res = instance.__dict__[self.name] = self.func(instance)
return res
( docs.djangoproject.com/en/1.9/_modules/django/utils/functional/… ).
Como puede ver, utiliza func.name para determinar el nombre de la función (no es necesario que juegue con inspect.stack) y reemplaza el método con su resultado mediante la mutación de la instance.__dict__
. Por lo tanto, las "llamadas" subsiguientes son solo una búsqueda de atributos y no hay necesidad de ningún caché, etc.
Puedes usar algo como esto:
def cached(timeout=None):
def decorator(func):
def wrapper(self, *args, **kwargs):
value = None
key = ''_''.join([type(self).__name__, str(self.id) if hasattr(self, ''id'') else '''', func.__name__])
if settings.CACHING_ENABLED:
value = cache.get(key)
if value is None:
value = func(self, *args, **kwargs)
if settings.CACHING_ENABLED:
# if timeout=None Django cache reads a global value from settings
cache.set(key, value, timeout=timeout)
return value
return wrapper
return decorator
Cuando se agrega al diccionario de caché, genera claves basadas en la convención class_id_function
en caso de que esté almacenando en caché las entidades y la propiedad posiblemente pueda devolver un valor diferente para cada una.
También comprueba una clave de configuración CACHING_ENABLED
en caso de que desee desactivarla temporalmente al realizar los puntos de referencia.
Pero no encapsula el decorador de property
estándar property
por lo que aún debería llamarlo como una función, o puede usarlo así (por qué restringirlo solo a propiedades):
@cached
@property
def total_sales(self):
# Some calculations here...
pass
También puede valer la pena tener en cuenta que, en caso de que esté almacenando en caché un resultado de relaciones de clave foránea perezosas, hay veces en función de sus datos donde sería más rápido simplemente ejecutar una función agregada al realizar su consulta de selección y obtener todo de una vez, que Visitando el caché para cada registro en su conjunto de resultados. Por lo tanto, use alguna herramienta como django-debug-toolbar
para su marco de trabajo para comparar lo que funciona mejor en su escenario.
Si no te importan las soluciones alternativas, te recomiendo lru_cache
por ejemplo
from functools import lru_cache
class Test:
@property
@lru_cache(maxsize=None)
def calc(self):
print("Calculating")
return 1
Rendimiento esperado
In [2]: t = Test()
In [3]: t.calc
Calculating
Out[3]: 1
In [4]: t.calc
Out[4]: 1