2016-05-30 131 views
1

我想爲類方法創建一個「緩存」裝飾器,它在內部類屬性中註冊避免計算多次的方法的結果(並且我不想使用一個簡單的屬性,在__init__中計算,因爲我不確定一直計算它)。Python裝飾器處理裝飾函數的默認參數

的第一個想法是創建一個裝飾「緩存」與此類似:

def cache(func): 
    name = "_{:s}".format(func.__name__) 
    def wrapped(obj): 
     if not hasattr(obj, name) or getattr(obj, name) is None: 
      print "Computing..." 
      setattr(obj, name, func(obj)) 
     else: 
      print "Already computed!" 
     return getattr(obj, name) 
    return wrapped 

class Test: 
    @cache 
    def hello(self): 
     return 1000 ** 5 

一切正常:

In [121]: t = Test() 

In [122]: hasattr(t, '_hello') 
Out[122]: False 

In [123]: t.hello() 
Computing... 
Out[123]: 1000000000000000 

In [124]: t.hello() 
Already computed! 
Out[124]: 1000000000000000 

In [125]: hasattr(t, '_hello') 
Out[125]: True 

現在讓我們說,我想要做同樣的事情,但是當方法可以用參數調用時(keyworded和/或不)。 當然,現在我們將把結果存儲在不同的屬性中(名字是什麼?),但是在一個字典中,它的鍵由* args和** kwargs組成。讓我們與元組做到這一點:

def cache(func): 
    name = "_{:s}".format(func.__name__) 
    def wrapped(obj, *args, **kwargs): 
     if not hasattr(obj, name) or getattr(obj, name) is None: 
      setattr(obj, name, {}) 
     o = getattr(obj, name) 
     a = args + tuple(kwargs.items()) 
     if not a in o: 
      print "Computing..." 
      o[a] = func(obj, *args, **kwargs) 
     else: 
      print "Already computed!" 
     return o[a] 
    return wrapped 

class Test: 
    @cache 
    def hello(self, *args, **kwargs): 
     return 1000 * sum(args) * sum(kwargs.values()) 

In [137]: t = Test() 

In [138]: hasattr(t, '_hello') 
Out[138]: False 

In [139]: t.hello() 
Computing... 
Out[139]: 0 

In [140]: hasattr(t, '_hello') 
Out[140]: True 

In [141]: t.hello(3) 
Computing... 
Out[141]: 0 

In [142]: t.hello(p=3) 
Computing... 
Out[142]: 0 

In [143]: t.hello(4, y=23) 
Computing... 
Out[143]: 92000 

In [144]: t._hello 
Out[144]: {(): 0, (3,): 0, (4, ('y', 23)): 92000, (('p', 3),): 0} 

多虧了事實的方法items變成了一個元組的字典,而不考慮在字典順序上,它完美的作品,如果keyworded參數不被調用同一訂單:

In [146]: t.hello(2, a=23,b=34) 
Computing... 
Out[146]: 114000 

In [147]: t.hello(2, b=34, a=23) 
Already computed! 
Out[147]: 114000 

這裏是我的問題:如果該方法具有默認參數,那麼它不工作了:

class Test: 
    @cache 
    def hello(self, a=5): 
     return 1000 * a 

現在不活像k:

In [155]: t = Test() 

In [156]: t.hello() 
Computing... 
Out[156]: 5000 

In [157]: t.hello(a=5) 
Computing... 
Out[157]: 5000 

In [158]: t.hello(5) 
Computing... 
Out[158]: 5000 

In [159]: t._hello 
Out[159]: {(): 5000, (5,): 5000, (('a', 5),): 5000} 

結果計算3次,因爲參數沒有以相同的方式給出(即使它們是「相同的」參數!)。

有人知道我怎麼可以捕捉給裝飾器內的函數的「默認」值?

謝謝

+0

你知道[functools.lru_cache](https://docs.python.org/3/library/functools.html#functools.lru_cache)嗎? –

+0

@Jonas Wielicki它在3.2以下的Python中不可用。他使用Python 2.x –

+0

@VadimShkaberda對,我沒有注意到它是在3.2中添加的。 –

回答

1

根據參數的功能結構有多複雜,可以有各種解決方案。我更喜歡的解決方案是將內部函數添加到hello中。如果你不想改變你的緩存的名字,給它同名的外部函數有:

class Test: 
    def hello(self, a=5): 
     @cache 
     def hello(self, a): 
      return 1000 * a 
     return hello(self, a) 

t = Test() 
t.hello() 
t.hello(a=5) 
t.hello(5) 
t._hello 

Out[111]: Computing... 
Already computed! 
Already computed! 
{(5,): 5000} 

另一種方法是添加爲您在裝飾默認的變量,例如:

def cache(func): 
    name = "_{:s}".format(func.__name__) 
    def wrapped(obj, *args, **kwargs): 
     if not hasattr(obj, name) or getattr(obj, name) is None: 
      setattr(obj, name, {}) 
     o = getattr(obj, name) 
     a = args + tuple(kwargs.items()) 
     if func.func_defaults: # checking if func have default variable 
      for k in kwargs.keys(): 
       if k in func.func_code.co_varnames and kwargs[k] == func.func_defaults[0]: 
        a =() 
      if args: 
       if args[0] == func.func_defaults[0]: 
        a =() 
     if not a in o: 
      print "Computing..." 
      o[a] = func(obj, *args, **kwargs) 
     else: 
      print "Already computed!" 
     return o[a] 
    return wrapped 

class Test: 
    @cache 
    def hello(self, a=5): 
     return 1000 * a 

t = Test() 
t.hello() 
t.hello(a=5) 
t.hello(5) 
t._hello 

Out[112]: Computing... 
Already computed! 
Already computed! 
{(): 5000} 

如果您有,例如2個默認變量,第一個代碼(具有內部函數)仍然可以工作,而第二個代碼需要在「默認變量檢查規則」中進行更改。

+0

感謝您的快速回答!我喜歡這兩種方式,但我想我會選擇第二個,這更符合我的需要! – Edouardb

1

如果您使用的是足夠新的Python版本,則可以使用inspect.signature來獲取完全封裝有關函數參數信息的Signature對象。然後你可以調用它的bind方法,使用你的包裝器傳遞的參數來得到一個BoundArguments對象。呼籲BoundArgumentsapply_defaults方法填補具有缺省值的任何缺少的參數,並檢查arguments有序字典看參數的功能和它們的值這個呼叫的明確上市:

import inspect 

def cache(func): 
    name = "_{:s}".format(func.__name__) 
    sig = inspect.signature(func) 
    def wrapped(obj, *args, **kwargs): 
     cache_dict = getattr(obj, name, None) 
     if cache_dict is None: 
      cache_dict = {} 
      setattr(obj, name, cache_dict)  
     bound_args = sig.bind(obj, *args, **kwargs) 
     bound_args.apply_defaults() 
     cache_key = tuple(bound_args.arguments.values()) 
     if not cache_key in cache_dict: 
      print("Computing...") 
      cache_dict[cache_key] = func(obj, *args, **kwargs) 
     else: 
      print("Already computed!") 
     return cache_dict[cache_key] 
    return wrapped 

注我將您的ao變量重命名爲具有更多有意義的名稱。我還改變了在對象上設置緩存字典的方式。更少的getattrsetattr這樣調用!

0123.函數和關聯類型是在Python 3.3中添加的,但BoundArguments對象上的apply_defaults方法在Python 3.5中是新的。有一箇舊版Python版本on PyPi的基本功能的回溯,但它似乎還不包括apply_defaults。我將在後臺的github tracker上報告這個問題。