2010-12-13 12 views
8

Próbuję zbudować dekorator dla metody instancji klasy, która zapamiętuje wynik. (Dokonano tego już milion razy wcześniej). Chciałbym jednak móc zresetować pamięć podręczną w dowolnym miejscu (powiedzmy, jeśli zmieni się stan w stanie instancji, co może zmienić wynik metody, która nie ma nic wspólnego). zrobić z jego argumentami). Tak więc, próbowałem zbudować dekorator jako klasę zamiast funkcji, tak żebym mógł mieć dostęp do pamięci podręcznej jako członek klasy. Doprowadziło mnie to do poznania deskryptorów, a konkretnie metody __get__, w której faktycznie utknąłem. Mój kod wygląda następująco:python resettable metoda instancji memoization decorator

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) 

Czy to jest jasne i/lub możliwe? Za każdym razem, gdy wywoływana jest nazwa __get__, otrzymuję zupełnie nową instancję klasy pamiętanej, która traci pamięć podręczną z rzeczywistymi danymi w niej. Ciężko pracowałem z __get__, ale nie robię dużego postępu.

Czy istnieje całkowicie oddzielne podejście do tego problemu, którego zupełnie mi brakuje? I wszystkie rady/sugestie są mile widziane i doceniane. Dzięki.

+0

nie głębiej w kodzie, ale dla lepszej wydajności należy użyć 'jeśli cache .has_key (...): return cache [...] 'zamiast przechwytywania' KeyError'. – khachik

+2

@khachik: 'klucz w pamięci podręcznej' jest lepszy, ponieważ' has_key' jest przestarzałe. – delnan

+0

Należy pamiętać, że celem funkcji przywołania jest to, że wywołanie funkcji z tymi samymi argumentami daje takie same wyniki. Jeśli naprawdę potrzebujesz zresetować pamięć podręczną na podstawie stanu instancji, może powinieneś rozważyć utrzymanie pamięci podręcznej w instancji zamiast pamiętanego dekoratora. – Falmarri

Odpowiedz

6

Zamiast próbować opracować mechanikę implementacji, pobrałem klasę dekoratorów memoized z PythonDecoratorLibrary i zmodyfikowałem ją, aby dodać reset. Poniżej znajduje się wynik; sztuczka, której użyłem, polega na dodaniu atrybutu wywoływalnego reset do samej funkcji dekorowanej.

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

Dzięki! Wypróbowałem podejście functools, ale nie miałem doświadczenia, aby zdać sobie sprawę, co właściwie robi. Zdaję sobie sprawę, że cały pomysł jest trochę niezręczny, więc dziękuję za pomoc! – Hoopes

+0

Może to zadziałać, ale wydaje mi się to bardzo nieefektywne, biorąc pod uwagę to, co robi '__get __()' przy każdym odwołaniu do metody zapamiętanej - co jest nieco ironiczne, biorąc pod uwagę zamierzone użycie ... – martineau

+0

@martineau - W interesie edukacji , dlaczego mówisz, że "__get__" jest nieefektywne? (Oryginalna ciekawość, jako początkujący) – Hoopes

0

Cóż, chciałbym wskazać dwa problemy z wydajnością w kodzie. To nie jest odpowiedź na twoje pytanie, ale nie mogę tego skomentować. Dzięki @delnan za wskazanie, że has_key jest przestarzałe. Zamiast:

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) 

chciałbym zrobić to w ten sposób:

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) 

ten sposób unika się: a) spróbuj/z wyjątkiem KeyError; b) wywołanie cache[key] po powrocie; c) wywołanie funkcji jeszcze raz na niezamocowanych klawiszach.

+2

Mylisz się co do spróbuj/z wyjątkiem bycia problemem z wydajnością. To prawda, że ​​wyjątki są drogie, ale tylko wtedy, gdy faktycznie wyzwalają: http://ideone.com/8lXyo W przypadku zapamiętywania, można oczekiwać, że trafienia w pamięci podręcznej będą znacznie częstsze niż te, które pominą. W przypadku braku pamięci podręcznej koszty ponownego przeliczenia najprawdopodobniej przekroczą koszt wyjątku. – lqc

+0

@lqc Właśnie powiedziałem, że 'try/except KeyError' jest wolniejszy niż' has_key' lub 'key in', nazywany raz lub 1000 razy. Nie jestem informowany o dystrybucji danych w domenie OP, przepraszam :) – khachik

2

Bazując na odpowiedzi na oryginalne pytanie udzielone przez @ aix, stworzyłem klasę, która moim zdaniem mogłaby ją poprawić. Główną cechą jest to, że buforowane wartości są przechowywane jako własność instancji, której metoda jest dekorowana, dlatego bardzo łatwo jest je zresetować.

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 

Jako przykład użycia:

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) 

dostaje jako wyjście:

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