2010-12-13 2 views
8

Je tente de construire un décorateur pour une méthode d'instance d'une classe qui mémoize le résultat. (Cela a été fait un million de fois auparavant) Cependant, je voudrais pouvoir réinitialiser le cache memoized à tout moment (par exemple, si quelque chose change dans l'état de l'instance, ce qui pourrait changer le résultat de la méthode n'ayant rien à faire avec ses args). J'ai donc essayé de construire un décorateur en tant que classe au lieu d'une fonction, de sorte que je puisse avoir accès au cache en tant que membre de la classe. Cela m'a conduit sur le chemin de l'apprentissage des descripteurs, en particulier la méthode __get__, qui est où je suis réellement coincé. Mon code ressemble à ceci:python réinitialisable instance méthode memoization décorateur

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) 

Est-ce clair et/ou possible? Chaque fois que __get__ est appelée, j'obtiens une nouvelle instance de la classe memoized, qui me perd le cache avec des données réelles. J'ai travaillé dur avec __get__, mais ne fais pas beaucoup de progrès.

Existe-t-il une approche complètement séparée de ce problème qui me manque complètement? Et tous les conseils/suggestions sont les bienvenus et appréciés. Merci.

+0

Je ne suis pas plus loin dans votre code, mais pour de meilleures performances, vous devriez utiliser 'si le cache .has_key (...): retourne le cache [...] 'au lieu d'attraper' KeyError'. – khachik

+2

@khachik: "key in cache" est meilleur, car 'has_key' est obsolète. – delnan

+0

Gardez à l'esprit que le point de mémoization 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 en fonction de l'état de l'instance, vous devriez peut-être envisager de conserver le cache dans l'instance au lieu d'un décorateur mémo. – Falmarri

Répondre

6

Plutôt que d'essayer de mettre au point les mécanismes de votre implémentation, j'ai pris la classe décorateur de PythonDecoratorLibrary, et je l'ai modifiée pour ajouter reset. Voici le résultat. l'astuce que j'ai utilisée consiste à ajouter un attribut reset pouvant être appelé à 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) 
+0

Merci! J'avais essayé l'approche de functools, mais je n'avais pas l'expérience pour réaliser ce qu'elle faisait réellement. Je me rends compte que l'idée est plutôt bizarre, alors merci pour votre aide! – Hoopes

+0

Cela peut fonctionner, mais cela me semble très inefficace compte tenu de ce que __get __() 'fait sur chaque référence à la méthode memoized - ce qui est quelque peu ironique compte tenu de l'usage prévu ... – martineau

+0

@martineau - Dans l'intérêt de l'éducation , pourquoi diriez-vous que le «__get__» est inefficace? (Véritable curiosité, en tant que débutant) – Hoopes

0

Eh bien, je tiens à 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 de souligner que has_key est obsolète. Au lieu de:

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) 

Je le ferais de cette façon:

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) 

Cela évite: a) try/except KeyError; b) appeler cache[key] au retour; c) appeler la fonction une fois de plus sur des touches non-cessibles.

+2

Vous avez tort d'essayer/sauf d'être un problème de performance. Il est vrai que les exceptions coûtent cher, mais seulement si elles déclenchent réellement: http://ideone.com/8lXyo En cas de mémorisation, on s'attendrait à ce que les caches de cache soient beaucoup plus fréquents que les échecs. En cas d'absence de cache, les coûts de recalculation remplacent probablement le coût d'une exception. – lqc

+0

@lqc Je viens de dire que 'try/except KeyError' est plus lent que' has_key' ou 'key in', l'a appelé une fois ou 1000 fois. Je ne suis pas informé de la distribution des données dans le domaine OP, désolé :) – khachik

2

En me basant sur la réponse à la question initiale donnée par @aix, j'ai créé une classe qui, je pense, pourrait l'améliorer. La caractéristique principale est que les valeurs mises en cache sont stockées en tant que propriété de l'instance dont la méthode est en cours de décoration, il est donc très facile de les réinitialiser.

class memoize(object): 
    def __init__(self, func): 
    #print "Init" 
    self.func = func 

    def __call__(self, *args): 
    #print "Call" 
    if not self.func in self.cache: 
     self.cache[self.func] = {} 
    try: 
     return self.cache[self.func][args] 
    except KeyError: 
     value = self.func(*args) 
     self.cache[self.func][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.""" 
    #print "Get", obj, objtype 
    fn = functools.partial(self.__call__, obj) 
    try: 
     self.cache = obj.cache 
    except: 
     obj.cache = {} 
     self.cache = obj.cache 
    #print self.cache 
    return fn 

À titre d'exemple d'utilisation:

class MyClass(object): 
    def __init__(self,data): 
     self.data = data 

    def update(self,data): 
     self.data = data 
     self.cache = {} 

    @memoize 
    def func1(self,x): 
     print "Computing func1" 
     return "I am func1 of %s. Data is %s. x is %s\n" % (self, self.data, x) 

    @memoize 
    def func2(self,x): 
     print "Computing func2" 
     return "I am func2 of %s. Data is %s. x is %s\n" % (self, self.data, x) 

    def func3(self,x): 
     print "Computing func3" 
     return "I am func3 of %s. Data is %s. x is %s\n" % (self, self.data, x) 

mc1 = MyClass("data1") 
mc2 = MyClass("data2") 
mc3 = MyClass("data3") 

print mc1.func1(1) 
print mc1.func1(1) 
print mc1.func2(1) 
print mc1.func2(1) 
print mc1.func3(1) 
print mc1.func3(1) 

print mc2.func1(1) 
print mc2.func1(1) 
print mc2.func2(1) 
print mc2.func2(1) 
print mc2.func3(1) 
print mc2.func3(1) 

print "Update mc1\n" 
mc1.update("data1new") 

print mc1.func1(1) 
print mc1.func2(1) 
print mc1.func3(1) 
print mc2.func1(1) 
print mc2.func2(1) 
print mc2.func3(1) 

obtient en sortie:

Computing func1 
I am func1 of <__main__.MyClass object at 0x100470fd0>. Data is data1. x is 1 

I am func1 of <__main__.MyClass object at 0x100470fd0>. Data is data1. x is 1 

Computing func2 
I am func2 of <__main__.MyClass object at 0x100470fd0>. Data is data1. x is 1 

I am func2 of <__main__.MyClass object at 0x100470fd0>. Data is data1. x is 1 

Computing func3 
I am func3 of <__main__.MyClass object at 0x100470fd0>. Data is data1. x is 1 

Computing func3 
I am func3 of <__main__.MyClass object at 0x100470fd0>. Data is data1. x is 1 

Computing func1 
I am func1 of <__main__.MyClass object at 0x100476050>. Data is data2. x is 1 

I am func1 of <__main__.MyClass object at 0x100476050>. Data is data2. x is 1 

Computing func2 
I am func2 of <__main__.MyClass object at 0x100476050>. Data is data2. x is 1 

I am func2 of <__main__.MyClass object at 0x100476050>. Data is data2. x is 1 

Computing func3 
I am func3 of <__main__.MyClass object at 0x100476050>. Data is data2. x is 1 

Computing func3 
I am func3 of <__main__.MyClass object at 0x100476050>. Data is data2. x is 1 

Update mc1 

Computing func1 
I am func1 of <__main__.MyClass object at 0x100470fd0>. Data is data1new. x is 1 

Computing func2 
I am func2 of <__main__.MyClass object at 0x100470fd0>. Data is data1new. x is 1 

Computing func3 
I am func3 of <__main__.MyClass object at 0x100470fd0>. Data is data1new. x is 1 

I am func1 of <__main__.MyClass object at 0x100476050>. Data is data2. x is 1 

I am func2 of <__main__.MyClass object at 0x100476050>. Data is data2. x is 1 

Computing func3 
I am func3 of <__main__.MyClass object at 0x100476050>. Data is data2. x is 1 
Questions connexes