Décorateur pour une méthode de classe qui met en cache la valeur de retour après le premier accès

Mon problème, et pourquoi

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 afin qu'il n'ait pas besoin d'être explicitement appelé. Fondamentalement, il devrait être indiscernable de @property sauf que c'est plus rapide, car il ne calcule la valeur qu'une seule fois et la stocke ensuite. Mon idée est que cela ne ralentirait pas l'instanciation comme le définir dans __init__. C'est pourquoi je veux faire ce.

Ce que j'ai essayé

Tout d'abord, j'ai essayé de remplacer la méthode fget du property, mais c'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 met ensuite en cache les valeurs. Ce n'est pas mon objectif final d'un décorateur de type propriété 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 mots, c'est un 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é ceci avec:

>>> class Test:
...     @cachedproperty
...     def test(self):
...             print "Execute"
...             return "Return"
... 
>>> Test.test
<unbound method Test.cache>
>>> Test.test()

Mais je reçois 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 Python profondes sont très confus, et je n'ai aucune idée où mon code a mal tourné ou comment le réparer. (Je n'ai jamais essayé d'écrire un décorateur avant)

La question

Comment puis-je écrire un décorateur qui retournera le résultat de l'appel d'une méthode de classe la première fois qu'elle est accédée (comme le fait @property), 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 aussi bien que possible.

21
demandé sur Luke Taylor 2016-04-18 04:55:49

5 réponses

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 retournons {[5] } pour faire en sorte que python fasse 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'
>>>

"""

8
répondu robyschek 2016-04-18 02:50:26

Si cela ne vous dérange pas des solutions alternatives, je recommanderais lru_cache

Par exemple

from functools import lru_cache
class Test:
    @property
    @lru_cache(maxsize=None)
    def calc(self):
        print("Calculating")
        return 1

Résultats escomptés

In [2]: t = Test()

In [3]: t.calc
Calculating
Out[3]: 1

In [4]: t.calc
Out[4]: 1
12
répondu 00500005 2016-04-18 11:48:19

Je pense que vous êtes mieux avec un descripteur personnalisé, car c'est exactement le genre de descripteurs de choses. Comme ça:

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: au fait, 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'il y a à faire.

Cependant, beaucoup considéreraient cela comme un abus de property, et comme une façon inattendue de l'utiliser. Et inattendu signifie généralement que vous devriez le faire une autre façon, plus explicite. Un descripteur CachedProperty personnalisé est très explicite, donc pour cette raison je le préférerais à l'approche property, bien qu'il nécessite plus de code.

3
répondu Rick Teachey 2016-04-18 07:58:56

, 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 dans le cas où vous mettez en cache des entités et que la propriété pourrait éventuellement renvoyer une valeur différente pour chacune d'elles.

Il vérifie également une touche de paramètres CACHING_ENABLED au cas où vous souhaiteriez l'éteindre temporairement lorsque vous effectuez des benchmarks.

Mais il n'encapsule pas le décorateur standard property donc vous devriez 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

En outre, il peut être intéressant de noter que dans le cas où vous mettez en cache un résultat à partir de relations de clés étrangères paresseuses, il y a des moments en fonction de vos données où il serait plus rapide d'exécuter simplement une fonction d'agrégat lorsque vous effectuez votre requête select et récupérez tout à la fois, Utilisez donc un outil comme django-debug-toolbar pour votre framework afin de comparer ce qui fonctionne le mieux dans votre scénario.

2
répondu fips 2016-04-18 02:41:02

La version de Django de ce décorateur fait exactement ce que vous décrivez et est simple, donc en plus de mon commentaire, je vais simplement 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 inspecter.stack) et il remplace la méthode par son résultat en Mutant instance.__dict__. Donc, les "appels" suivants ne sont qu'une recherche d'attribut et il n'y a pas besoin de caches, et cetera.

1
répondu RemcoGerlich 2016-04-18 11:44:22