2010-12-13 16 views
8

Estoy intentando construir un decorador para un método de instancia de una clase que memorizará el resultado. (Esto se ha hecho un millón de veces antes) Sin embargo, me gustaría la opción de poder restablecer el caché memorable en cualquier punto (por ejemplo, si algo en el estado de la instancia cambia, lo que podría cambiar el resultado del método sin tener nada para hacer con sus argumentos). Entonces, intenté construir un decorador como una clase en lugar de una función, para poder tener acceso a la memoria caché como miembro de la clase. Esto me llevó por el camino de aprender sobre los descriptores, específicamente el método __get__, que es donde estoy realmente atrapado. Mi código es el siguiente manera:instancia de python que restablece el método de memoial decorador

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á claro y/o posible? Cada vez que se llama al __get__, obtengo una nueva instancia de la clase memoial, que me pierde el caché con los datos reales. He estado trabajando duro con __get__, pero no estoy progresando mucho.

¿Hay un enfoque completamente separado de este problema que me falta por completo? Y todos los consejos/sugerencias son bienvenidos y apreciados. Gracias.

+0

no fui más profundo en su código, pero para un mejor rendimiento que debe utilizar 'si caché .has_key (...): devuelve el caché [...] 'en lugar de capturar' KeyError'. – khachik

+2

@khachik: 'key in cache' es mejor, ya que' has_key' está en desuso. – delnan

+0

Tenga en cuenta que el punto de memorización es que una llamada a función con los mismos argumentos produce el mismo resultado. Si realmente necesita restablecer el caché en función del estado de la instancia, tal vez debería considerar conservar el caché en la instancia en lugar de un decorador con memoria. – Falmarri

Respuesta

6

En lugar de tratar de resolver la mecánica de su implementación, tomé la clase de decorador memoized de PythonDecoratorLibrary, y la modifiqué para agregar reset. A continuación está el resultado; el truco que he usado es agregar un atributo llamable reset a la función decorada.

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

Gracias! Probé el enfoque de functools, pero no tenía la experiencia para darme cuenta de lo que realmente estaba haciendo. Me doy cuenta de que toda la idea es un poco incómoda, ¡así que gracias por tu ayuda! – Hoopes

+0

Esto puede funcionar, pero me parece muy ineficiente, dado lo que '__get __()' está haciendo en cada referencia al método memorizado, lo cual es algo irónico considerando el uso previsto ... – martineau

+0

@martineau - En interés de la educación , ¿por qué dirías que el '__get__' es ineficiente? (Curiosidad genuina, como un principiante) – Hoopes

0

Bueno, me gustaría señalar dos problemas de rendimiento en su código. Esta no es una respuesta a su pregunta, pero no puedo hacer un comentario. Gracias a @delnan por señalar que has_key está en desuso. En lugar 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) 

que lo haría de esta manera:

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) 

Esto evita: a) tratar/excepto KeyError; b) llamar al cache[key] en el momento del regreso; c) llamar a la función una vez más en claves que no se pueden cambiar.

+2

Estás equivocado sobre try/excepto que es un problema de rendimiento. Es cierto que las excepciones son costosas, pero solo si realmente se disparan: http://ideone.com/8lXyo En caso de memorización, uno esperaría que los hits de caché fueran mucho más frecuentes que los errores. En el caso de una falta de caché, es muy probable que los costos de recalcular superen el costo de capturar una excepción. – lqc

+0

@lqc Acabo de decir que 'try/except KeyError' es más lento que' has_key' o 'key in', lo llamó una vez o 1000 veces. No estoy informado sobre la distribución de datos en el dominio OP, lo siento :) – khachik

2

Basándome en la respuesta a la pregunta original formulada por @aix Creé una clase que creo que podría mejorarla. La característica principal es que los valores en caché se almacenan como una propiedad de la instancia cuyo método se está decorando, por lo que es muy fácil restablecerlos.

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 

Como un ejemplo de uso:

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) 

obtiene como salida:

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 
Cuestiones relacionadas