10
votes

Décorateur de mémoisation de méthode de méthode Python Réinter

Je tente de construire un décorateur pour une méthode d'instance d'une classe qui tiendra le résultat. (Cela a été fait un million de fois auparavant) Cependant, j'aimerais pouvoir réinitialiser le cache mémo à tout moment (par exemple, si quelque chose dans l'état d'instance change, ce qui pourrait changer le résultat de la méthode n'ayant rien faire avec ses arguments). Ainsi, j'ai tenté de construire un décorateur comme une classe au lieu d'une fonction, de sorte que je pourrais avoir accès au cache comme membre de classe. Cela m'a conduit sur le chemin d'apprentissage sur les descripteurs, en particulier le __ obtenez __ code> méthode, ce qui est en fait bloqué. Mon code ressemble à:

import time

class memoized(object):

    def __init__(self, func):
        self.func = func
        self.cache = {}

    def __call__(self, *args, **kwargs):

        key = (self.func, args, frozenset(kwargs.iteritems()))

        try:
            return self.cache[key]
        except KeyError:
            self.cache[key] = self.func(*args, **kwargs)
            return self.cache[key]
        except TypeError:
            # uncacheable, so just return calculated value without caching
            return self.func(*args, **kwargs)

    # self == instance of memoized
    # obj == instance of my_class
    # objtype == class object of __main__.my_class
    def __get__(self, obj, objtype=None):
        """Support instance methods"""
        if obj is None:
            return self

        # new_func is the bound method my_func of my_class instance
        new_func = self.func.__get__(obj, objtype)

        # instantiates a brand new class...this is not helping us, because it's a 
        # new class each time, which starts with a fresh cache
        return self.__class__(new_func)

    # new method that will allow me to reset the memoized cache
    def reset(self):
        print "IN RESET"
        self.cache = {}

class my_class:
    @memoized
    def my_func(self, val):
        print "in my_func"
        time.sleep(2)
        return val


c = my_class()

print "should take time"
print c.my_func(55)
print

print "should be instant"
print c.my_func(55)
print

c.my_func.reset()

print "should take time"
print c.my_func(55)


5 commentaires

Je ne suis pas allé plus profondément dans votre code, mais pour de meilleures performances, vous devez utiliser si cache.has_key (...): renvoyer le cache [...] au lieu de capture KeyError .


@khachik: Touche dans le cache est meilleur, car has_key est obsolète.


N'oubliez pas que le point de mémoisation est qu'un appel de fonction avec les mêmes arguments produit la même sortie. Si vous avez vraiment besoin de réinitialiser le cache basé sur l'état de l'instance, vous devriez peut-être envisager de garder le cache dans l'instance au lieu d'un décorateur mémoqué.


@khachik @delnan - Ouais, j'aurais dû le poster complètement nettoyé. J'essayais juste d'avoir le concept. Désolé. @Falmarri - Je me rends compte que cela est conceptuellement gênant, mais j'espérais simplement être capable d'ajouter un décorateur (1 ligne), au lieu de faire toute la vérification de la cache (disons, 5 lignes) par méthode utilisée. Peut-être que je vais Revenez et découvrez comment procéder succinctement dans l'instance. Merci à tous!


@Falmarri: Je pense que je devrais être en désaccord avec cette évaluation. Il existe de nombreuses applications dans lesquelles vous souhaitez mémoriser des fonctions en fonction du contexte de l'application, puis invalider les caches lorsque vous avez quitté le contexte (par exemple, des requêtes de base de données, où l'application peut se déconnecter de la base de données et se connecter à un nouveau). Personne n'a jamais dit que la mémoisation devait rester valable pour l'ensemble de la demande. Il doit juste rester valable suffisamment longtemps pour vous donner un avantage de performance significatif.


3 Réponses :


6
votes

Plutôt que d'essayer de résoudre la mécanique de votre implémentation, j'ai pris la classe de décorateur code> code> de PythondecoratoratorLibrary et l'ont modifié pour ajouter réinitialiser code>. Ci-dessous est le résultat; L'astuce que j'ai utilisée consiste à ajouter un attribut appelable réinitialiser code> à la fonction décorée elle-même.

    class memoized2(object):
       """Decorator that caches a function's return value each time it is called.
       If called later with the same arguments, the cached value is returned, and
       not re-evaluated.
       """
       def __init__(self, func):
          self.func = func
          self.cache = {}
       def __call__(self, *args):
          try:
             return self.cache[args]
          except KeyError:
             value = self.func(*args)
             self.cache[args] = value
             return value
          except TypeError:
             # uncachable -- for instance, passing a list as an argument.
             # Better to not cache than to blow up entirely.
             return self.func(*args)
       def __repr__(self):
          """Return the function's docstring."""
          return self.func.__doc__
       def __get__(self, obj, objtype):
          """Support instance methods."""
          fn = functools.partial(self.__call__, obj)
          fn.reset = self._reset
          return fn
       def _reset(self):
          self.cache = {}


    class my_class:
        @memoized2
        def my_func(self, val):
            print "in my_func"
            time.sleep(2)
            return val


    c = my_class()

    print "should take time"
    print c.my_func(55)
    print

    print "should be instant"
    print c.my_func(55)
    print

    c.my_func.reset()

    print "should take time"
    print c.my_func(55)


11 commentaires

Merci! J'avais essayé l'approche FuncTools, mais n'avait pas l'expérience de réaliser ce que cela faisait réellement. Je réalise que toute l'idée est un peu maladroite, alors merci pour votre aide!


Cela peut fonctionner, mais il a l'air très inefficace pour moi donné ce que __ obtenez __ () fait sur chaque référence à la méthode commémorée - qui est quelque peu ironique compte tenu de l'utilisation prévue ...


@Martineau - Dans l'intérêt de l'éducation, pourquoi diriez-vous que le __ obtenir __ est inefficace? (Véritable curiosité, comme débutant)


@Hoopes: ok sûr. J'ai dit que parce que sur chaque Access de la méthode mémozaine avant toute mise en cache ait eu la chance de se dérouler, il appelle FuncTools.Partial () qui crée une fonction qui sera appelée Et puis, juste au cas où, il crée et attribue également self._reset à un attribut de cette fonction nouvellement créée afin que ce soit là s'il s'appelle. Je pense que cela peut être une limitation inhérente à essayer de cacher des choses via le protocole descripteur qui ne sait pas pourquoi ou pour quel but vous obtenez, définir ou supprimer sa valeur logique.


@martineau, s'il vous plaît corrigez-moi si je me trompe, mais il me semble une solution facile: après la construction ci-dessus partielle , mettez-le dans un auto .__ wrapper et sur n'importe quelle suivante __ Obtenir __ Les appels renvoient le Cached auto .__ wrapper . Je semble travailler pour moi. Non?


@ K3 --- RNC: Bonne pensée. Cela fonctionne et réduit considérablement les frais généraux d'appeler de manière significative une méthode mémotionnée. BTW Je ne vois pas de besoin réel de doubler (ou même d'un seul) principal (s) de soulignement (s) sur tout nouveau " wrapper " attribut.


@ K3 --- RNC: En outre, BTW, je trouve "Methinks" juste une façon vraiment prétentieuse et gênante de dire "je pense".


@martineau, je suis d'accord. J'aime juste préfixer les attributs "internes" avec un soulignement comme je l'entends est une sorte de convention. En outre, je ne suis pas un orateur anglais natif, et je trouve des bizarreries archaïques telles que " Methinks " un ajout de bienvenue à ce qui est sinon une langue terne et logique. :RÉ


@ K3 --- RNC: J'espère ce que dit Ye. Je comprends que tu aies envie d'utiliser un soulignement de premier plan bien que l'utilisation de tels autres soit incompatible avec le code de code existant ici. De plus, en utilisant deux d'entre eux a une signification particulière dans Python qui ne s'applique pas ici et pourrait même être considérée comme trompeuse.


Comment accéderiez-vous à la méthode si @ mémoized2 a été utilisé sur un @property , c'est-à-dire @property \ n @ mémoized2 \ ndef myProp (auto): ?


Veuillez noter que cela réinitialisera le cache pour toutes les instances de "my_class".



0
votes

Eh bien, je voudrais souligner deux problèmes de performance dans votre code. Ce n'est pas une réponse à votre question, mais je ne peux pas en faire un commentaire. Merci à @delnan pour avoir souligné que has_key code> est obsolète. Au lieu de:

resultDone = False
result = None
try:
  if key in self.cache: return self.cache[key]
  else:
    result = self.func(*args, **kwargs)
    resultDone = True
    self.cache[key] = result
except TypeError: # unhashable key
  pass
if resultDone: return result
else: return self.func(*args, **kwargs)


2 commentaires

Vous avez tort d'essayer / sauf d'être un problème de performance. Il est vrai que des exceptions sont chères, mais seulement si elles déclenchent réellement: Ideone.com/8LXYO en cas de mémorisation, On pourrait s'attendre à ce que les hits de cache soient beaucoup plus fréquents puis manquent. En cas de cache Miss, les coûts de recalculition remplacent probablement le coût de la capture d'une exception.


@LQC I Je viens de dire que ESSAY / SAUF KeyError est plus lent que has_key ou de la touche , l'appelle une fois ou 1000 fois. Je ne suis pas informé de la distribution des données dans le domaine OP, désolé :)



4
votes

Construire sur la réponse à la question originale donnée par @aix j'ai créé une classe à laquelle je pense pourrait l'améliorer. La caractéristique principale est que les valeurs en cache sont stockées comme une propriété de l'instance dont la méthode est décorée, il est donc très facile de les réinitialiser. XXX

Comme exemple d'utilisation: xxx

obtient comme sortie: xxx


0 commentaires