J'essaie d'écrire un décorateur pour une méthode de classe, @cachedproperty
. Je veux qu'il se comporte de sorte que lorsque la méthode est appelée pour la première fois, la méthode soit remplacée par sa valeur de retour. Je veux aussi qu'il se comporte comme @property
de sorte qu'il n'a pas besoin d'être appelé explicitement. Fondamentalement, il devrait être impossible de le distinguer de @property
sauf que c'est plus rapide, car il ne calcule la valeur qu'une seule fois puis la stocke. Mon idée est que cela ne ralentirait pas l'instanciation comme la définissant dans __init__
le ferait. C'est pourquoi je veux faire ça.
Tout d'abord, j'ai essayé de remplacer la méthode fget
de property
, mais elle est en lecture seule.
Ensuite, j'ai pensé que j'essaierais d'implémenter un décorateur qui doit être appelé la première fois, mais qui met ensuite les valeurs en cache. Ce n'est pas mon objectif final d'un décorateur de type immobilier qui n'a jamais besoin d'être appelé, mais je pensais que ce serait un problème plus simple à résoudre en premier. En d'autres termes, il s'agit d'une solution non fonctionnelle à un problème légèrement plus simple.
J'ai essayé:
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
Cependant, cela ne semble pas fonctionner. J'ai testé cela avec:
>>> class Test:
... @cachedproperty
... def test(self):
... print "Execute"
... return "Return"
...
>>> Test.test
<unbound method Test.cache>
>>> Test.test()
mais j'obtiens une erreur sur la façon dont la classe ne s'est pas transmise à la méthode:
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)
À ce stade, moi et ma connaissance limitée des méthodes profondes Python sont très confuses, et je n'ai aucune idée où mon code a mal tourné ni comment y remédier. (Je n'ai jamais essayé d'écrire un décorateur avant)
Comment puis-je écrire un décorateur qui retournera le résultat de l'appel d'une méthode de classe lors du premier accès (comme @property
le fait) et être remplacé par une valeur mise en cache pour toutes les requêtes suivantes?
J'espère que cette question n'est pas trop confuse, j'ai essayé de l'expliquer du mieux que je pouvais.
Tout d'abord, Test
doit être instancié
test = Test()
Deuxièmement, il n'y a pas besoin de inspect
car nous pouvons obtenir le nom de la propriété à partir de func.__name__
Et troisièmement, nous renvoyons property(cache)
pour créer python = faire toute la magie.
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'
>>>
"" "
Si cela ne vous dérange pas, je vous recommande lru_cache
par exemple
from functools import lru_cache
class Test:
@property
@lru_cache(maxsize=None)
def calc(self):
print("Calculating")
return 1
Production attendue
In [2]: t = Test()
In [3]: t.calc
Calculating
Out[3]: 1
In [4]: t.calc
Out[4]: 1
Je pense que vous êtes mieux avec un descripteur personnalisé, car c'est exactement le genre de chose à laquelle les descripteurs sont destinés. Ainsi:
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"
EDIT: Soit dit en passant, vous n'avez même pas besoin d'un descripteur personnalisé. Vous pouvez simplement mettre en cache la valeur à l'intérieur de votre fonction de propriété. Par exemple.:
@property
def test(self):
while True:
try:
return self._test
except AttributeError:
self._test = get_initial_value()
C'est tout ce qu'on peut en dire.
Cependant, beaucoup considéreraient cela comme un peu un abus de property
, et comme une manière inattendue de l'utiliser. Et inattendu signifie généralement que vous devriez le faire d'une autre manière, plus explicite. Un descripteur CachedProperty
personnalisé est très explicite, c'est pourquoi je le préférerais à l'approche property
, bien qu'il nécessite plus de code.
Vous pouvez utiliser quelque chose comme ceci:
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
Lors de l'ajout au dictionnaire de cache, il génère des clés basées sur la convention class_id_function
au cas où vous mettriez en cache des entités et que la propriété pourrait éventuellement renvoyer une valeur différente pour chacune.
Il vérifie également une clé de paramètres CACHING_ENABLED
au cas où vous souhaiteriez le désactiver temporairement lors des tests de performances.
Mais il n'encapsule pas le décorateur standard property
, vous devriez donc toujours l'appeler comme une fonction, ou vous pouvez l'utiliser comme ceci (pourquoi le restreindre aux propriétés uniquement):
@cached
@property
def total_sales(self):
# Some calculations here...
pass
Il peut également être intéressant de noter que dans le cas où vous mettez en cache un résultat de relations de clés étrangères paresseux, il y a des moments selon vos données où il serait plus rapide d'exécuter simplement une fonction d'agrégation lors de votre requête de sélection et de tout récupérer à la fois, que visiter le cache pour chaque enregistrement de votre jeu de résultats. Utilisez donc un outil comme Django-debug-toolbar
pour que votre framework compare ce qui fonctionne le mieux dans votre scénario.
La version Django de ce décorateur fait exactement ce que vous décrivez et est simple, donc en plus de mon commentaire, je vais le copier ici:
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
( source ).
Comme vous pouvez le voir, il utilise func.name pour déterminer le nom de la fonction (pas besoin de jouer avec inspect.stack) et il remplace la méthode par son résultat en mutant instance.__dict__
. Les "appels" ultérieurs ne sont donc qu'une recherche d'attributs et il n'y a pas besoin de caches, et cetera.