subclases resueltos privados poo polimorfismo importar herencia ejercicios constructores clases atributos python caching decorator python-decorators

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