2014-10-29 58 views
7

我有一個簡單的小修飾器,它將函數調用中的函數作爲函數屬性緩存在dict中。重新分配函數屬性使其'不可達'

from decorator import decorator 
def _dynamic_programming(f, *args, **kwargs): 
    try: 
     f.cache[args] 
    except KeyError: 
     f.cache[args] = f(*args, **kwargs) 
    return f.cache[args] 

def dynamic_programming(f): 
    f.cache = {} 
    return decorator(_dynamic_programming, f) 

我現在想添加清空緩存的可能性。所以我改變dynamic_programming()功能是這樣的:

def dynamic_programming(f): 
    f.cache = {} 
    def clear(): 
     f.cache = {} 
    f.clear = clear 
    return decorator(_dynamic_programming, f) 

現在讓我們假設我用這個小東西,以實現一個斐波那契數函數:

@dynamic_programming 
def fib(n): 
    if n <= 1: 
     return 1 
    else: 
     return fib(n-1) + fib(n-2) 

>>> fib(4) 
5 
>>> fib.cache 
{(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5} 

但現在當我清除緩存奇怪的事情發生了:

>>> fib.clear() 
>>> fib.cache 
{(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5} 

或(有一個新的Python內核運行)這樣做反過來:

>>> fib.clear() 
>>> fib(4) 
5 
>>> fib.cache 
{} 

爲什麼緩存莫名其妙不是第一次訪問之後「到達」呼叫來電或clear()後一個電話後clear()時,即不改變?

(順便說一句,我知道了解決正確清除緩存:調用f.cache.clear(),而不是分配給{}它的工作如預期,我在原因爲什麼分配解決方案失敗僅僅是感興趣。)

回答

7

問題出在decorator模塊。如果一些print語句添加到您的裝飾:

from decorator import decorator 
def _dynamic_programming(f, *args, **kwargs): 
    print "Inside decorator", id(f.cache) 
    try: 
     f.cache[args] 
    except KeyError: 
     f.cache[args] = f(*args, **kwargs) 
    return f.cache[args] 

def dynamic_programming(f): 
    f.cache = {} 
    print "Original cache", id(f.cache) 
    def clear(): 
     f.cache = {} 
     print "New cache", id(f.cache) 
    f.clear = clear 
    return decorator(_dynamic_programming, f) 

@dynamic_programming 
def fib(n): 
    if n <= 1: 
     return 1 
    else: 
     return fib(n-1) + fib(n-2) 

print fib(4) 
print id(fib.cache) 
fib.clear() 
print id(fib.cache) 
print fib(10) 
print id(fib.cache) 

它輸出(重複的行跳過):

Original cache 139877501744024 
Inside decorator 139877501744024 
5 
139877501744024 
New cache 139877501802208 
139877501744024 
Inside decorator 139877501802208 
89 
139877501744024 

正如你所看到的,cache根據明確的功能裝飾的變化中。但是,從__main__訪問的cache不會更改。外面打印cache和裝飾內給出一個更清晰的畫面(再次,重複跳過):

Inside decorator {} 
Inside decorator {(1,): 1} 
Inside decorator {(2,): 2, (0,): 1, (1,): 1} 
Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1} 
5 
Outside {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5} 
Inside decorator {} 
Inside decorator {(1,): 1} 
Inside decorator {(2,): 2, (0,): 1, (1,): 1} 
Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1} 
Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5} 
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8} 
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8, (6,): 13} 
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8, (6,): 13, (7,): 21} 
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (8,): 34, (3,): 3, (4,): 5, (5,): 8, (6,): 13, (7,): 21} 
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (8,): 34, (3,): 3, (9,): 55, (4,): 5, (5,): 8, (6,): 13, (7,): 21} 
89 
Outside {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5} 

正如你可以看到,裏面的變化不回在外面。問題是,裏面the decorator module,有(類裏面用來做裝飾)線:

self.dict = func.__dict__.copy() 

然後later

func.__dict__ = getattr(self, 'dict', {}) 

所以基本上,__dict__在外面不同於裏面的__dict__。這意味着:

  • __dict__被複制(未標記)由_dynamic_programming使用的裝飾
  • cache改變,它改變了內部__dict__,而不是外__dict__
  • 因此,cache被清除,但是你不能從外面看到,因爲裝飾器的__dict__仍然指向舊的cache(如上所示,因爲內部的cache更新,而外部的cache保持不變)

因此,總而言之,這是decorator模塊的問題。

+2

我最長的職位尚未對SO ,由它的外觀... :) – matsjoyce 2014-10-29 17:15:47

3

所以@ matsjoyce的回答是非常有趣和深入,我知道你已經有了解決方案,但我總能找到更清楚一點寫我自己的裝飾:

def dynamic_programming(f): 
    def wrapper(*args, **kwargs): 
     try: 
      return wrapper.cache[args]    
     except KeyError: 
      res = wrapper.cache[args] = f(*args, **kwargs) 
      return res 
    wrapper.cache = {} 
    wrapper.clear = wrapper.cache.clear 
    return wrapper 
+2

好點。有了這樣的問題,無需挖掘模塊的源代碼就可以看到任何奇怪的東西...... – matsjoyce 2014-10-29 19:16:12