2012-08-06 54 views
6

以下函數用作存儲已計算值結果的裝飾器。 (錯誤),其cache不必是函數對象的屬性,我意識到在裝飾函數的多個調用中,數據如何保持持久性?

def cached(f): 
    f.cache = {} 
    def _cachedf(*args): 
     if args not in f.cache: 
      f.cache[args] = f(*args) 

     return f.cache[args] 

    return _cachedf 

:如果參數之前已經計算出,該函數將返回存儲在cache字典中的價值。作爲事實的問題,下面的代碼工作,以及:

def cached(f): 
    cache = {} # <---- not an attribute this time! 
    def _cachedf(*args): 
     if args not in cache: 
      cache[args] = f(*args) 

     return cache[args] 
    return _cachedf 

我有一個很難理解怎麼能cache對象是跨多個調用持久。我嘗試多次調用多個緩存函數,但無法找到任何衝突或問題。

任何人都可以請幫我理解cache變量在_cachedf函數返回後仍然存在嗎?

回答

11

您在此處創建closure:函數_cachedf()在封閉範圍內關閉變量cache。只要函數對象存在,這會使cache保持活動狀態。

編輯:也許我應該添加一些關於它如何在Python中工作的細節以及CPython如何實現這一點。

讓我們看一個簡單的例子:

def f(): 
    a = [] 
    def g(): 
     a.append(1) 
     return len(a) 
    return g 

用法示例在交互式解釋

>>> h = f() 
>>> h() 
1 
>>> h() 
2 
>>> h() 
3 

在包含該功能f()模塊的編寫, 編譯器看到,該函數g()引用名稱a來自 封閉示波器並將此外部參考存儲在代碼 object cor響應功能f()(具體而言,它將 名稱a添加到f.__code__.co_cellvars)。

那麼調用函數f()時會發生什麼?第一行 創建一個新的列表對象並將其綁定到名稱a。下一行 創建一個新的函數對象(使用在編譯該模塊期間創建的代碼對象),並將其綁定到名稱gg()的主體 此時不執行,最後返回功能對象 。

由於f()代碼對象有一張紙條,上面的名字是a通過 本地函數引用,當進入 f()創建一個「細胞」這個名字。該單元格包含對實際列表的引用 對象a被綁定到,並且函數g()獲取對該單元格的引用。 。這樣,當函數f()退出時,列表對象和單元格保持有效,即使是 。

+0

非常感謝您的解釋,您的編輯使事情變得非常清楚。我想知道,爲了學習(C)Python的內部機制,是否可以通過檢查或類似的方式訪問您在最後一段中提到的「單元格」? – rahmu 2012-08-06 15:27:58

+0

@rahmu:我解釋了一個錯誤(不會變化太大)。不幸的是,這些單元對於Python代碼來說是完全透明的,並且總是被它們引用的對象替換,所以它們不能被檢查。 – 2012-08-06 16:13:08

3

任何人都可以請幫助我理解即使在_cachedf函數返回後緩存變量仍然存在嗎?

它與Python的引用計數垃圾回收器有關。 cache變量將被保留並可訪問,因爲函數_cachedf有一個對它的引用,並且調用者cached對此有引用。當您再次調用該函數時,仍然使用最初創建的相同函數對象,因此您仍然可以訪問緩存。

在所有引用都被銷燬之前,您不會丟失緩存。您可以使用del運算符來完成此操作。

例如:

>>> import time 
>>> def cached(f): 
...  cache = {} # <---- not an attribute this time! 
...  def _cachedf(*args): 
...   if args not in cache: 
...    cache[args] = f(*args) 
...   return cache[args] 
...  return _cachedf 
...  
... 
>>> def foo(duration): 
...  time.sleep(duration) 
...  return True 
...  
... 
>>> bob = cached(foo) 
>>> bob(2) # Takes two seconds 
True 
>>> bob(2) # returns instantly 
True 
>>> del bob # Deletes reference to bob (aka _cachedf) which holds ref to cache 
>>> bob = cached(foo) 
>>> bob(2) # takes two seconds 
True 
>>> 

爲了記錄在案,你想acheive所謂Memoization,並有可從中做同樣的事情的decorator pattern page更完整memoizing裝飾,但使用裝飾者類別。你的代碼和基於類的修飾器基本上是一樣的,基於類的修飾器在存儲之前檢查散列能力。


編輯(2017年2月2日):@SiminJie評論說cached(foo)(2)總是招致延遲。

這是因爲cached(foo)返回一個帶有新緩存的新函數。當調用cached(foo)(2)時,會創建一個新的(空)緩存,然後立即調用緩存的函數。

由於緩存是空的並且不會找到該值,因此它會重新運行基礎函數。相反,請執行cached_foo = cached(foo),然後多次撥打cached_foo(2)。這隻會導致第一次電話的延遲。另外,如果用作裝飾,它會按預期工作:

@cached 
def my_long_function(arg1, arg2): 
    return long_operation(arg1,arg2) 

my_long_function(1,2) # incurs delay 
my_long_function(1,2) # doesn't 

如果你不熟悉的裝飾,看看this answer理解上面的代碼是什麼意思。

+0

這是如何工作的Python裝飾?每次我調用'cached(foo)(2)'時,它都不會緩存結果並休眠兩秒鐘。裝飾函數的每次調用是否引用同一個裝飾器? – 2017-02-02 08:19:49

+0

@SiminJie - 查看我對答案的額外編輯。 – brice 2017-02-02 17:23:02