2012-06-06 71 views
6

所以我最近問了一個關於memoization的問題,並得到了一些很好的答案,現在我想把它提升到一個新的水平。經過相當多的搜索後,我找不到一個能夠緩存帶關鍵字參數的函數的memoize裝飾器的參考實現。事實上,大多數人只是用*args作爲緩存查找的關鍵,這意味着它也將打破,如果你想memoize的接受了列表或類型的字典作爲參數的函數。Python是否有支持memoize裝飾器的關鍵字參數的pythonic方法?

在我的情況下,函數的第一個參數本身就是一個唯一標識符,適合用作緩存查找的詞典鍵,但是我希望能夠使用關鍵字參數並仍然訪問相同的緩存。我的意思是,my_func('unique_id', 10)my_func(foo=10, func_id='unique_id')都應該返回相同的緩存結果。

爲了做到這一點,我們需要的是說「檢查是否有哪個關鍵字kwargs它是對應於第一個參數)」的清潔,pythonic的方法。這是我想出了:

class memoize(object): 
    def __init__(self, cls): 
     if type(cls) is FunctionType: 
      # Let's just pretend that the function you gave us is a class. 
      cls.instances = {} 
      cls.__init__ = cls 
     self.cls = cls 
     self.__dict__.update(cls.__dict__) 

    def __call__(self, *args, **kwargs): 
     """Return a cached instance of the appropriate class if it exists.""" 
     # This is some dark magic we're using here, but it's how we discover 
     # that the first argument to Photograph.__init__ is 'filename', but the 
     # first argument to Camera.__init__ is 'camera_id' in a general way. 
     delta = 2 if type(self.cls) is FunctionType else 1 
     first_keyword_arg = [k 
      for k, v in inspect.getcallargs(
       self.cls.__init__, 
       'self', 
       'first argument', 
       *['subsequent args'] * (len(args) + len(kwargs) - delta)).items() 
        if v == 'first argument'][0] 
     key = kwargs.get(first_keyword_arg) or args[0] 
     print key 
     if key not in self.cls.instances: 
      self.cls.instances[key] = self.cls(*args, **kwargs) 
     return self.cls.instances[key] 

瘋狂的事情是,這實際工作。例如,如果你裝點這樣的:

@memoize 
class FooBar: 
    instances = {} 

    def __init__(self, unique_id, irrelevant=None): 
     print id(self) 

然後從你的代碼,你可以調用FooBar('12345', 20)FooBar(irrelevant=20, unique_id='12345')真正得到FooBar的的同一個實例。然後,您可以爲第一個參數定義一個不同名稱的不同類,因爲它以一般方式工作(即,裝飾器不需要知道關於正在裝飾的類的任何特定信息)。

的問題是,這是一個不敬虔的混亂;-)

它的工作原理,因爲inspect.getcallargs返回一個字典映射定義的關鍵字爲您提供它的參數,所以我爲它供給了一些假的論點,然後檢查的字典第一個參數通過了。

如果這樣的事情甚至存在,什麼會更好,是inspect.getcallargs的類似物,它將兩種參數統一爲參數列表,而不是作爲關鍵字參數的詞典。這將允許這樣的事情:

def __call__(self, *args, **kwargs): 
    key = inspect.getcallargsaslist(self.cls.__init__, None, *args, **kwargs)[1] 
    if key not in self.cls.instances: 
     self.cls.instances[key] = self.cls(*args, **kwargs) 
    return self.cls.instances[key] 

的另一種方式,我可以看到解決這個將使用由inspect.getcallargs提供直接查找緩存鍵的字典內,但這需要一個可重複的方式,利用相同的字符串相同的散列,這是我聽到的東西不能依賴(我想我必須在排序鍵後自己構造字符串)。

有沒有人對此有任何想法?想用關鍵字參數調用一個函數並緩存結果是錯誤的嗎?或者只是非常困難?

回答

4

我建議類似如下:

import inspect 

class key_memoized(object): 
    def __init__(self, func): 
     self.func = func 
     self.cache = {} 

    def __call__(self, *args, **kwargs): 
     key = self.key(args, kwargs) 
     if key not in self.cache: 
      self.cache[key] = self.func(*args, **kwargs) 
     return self.cache[key] 

    def normalize_args(self, args, kwargs): 
     spec = inspect.getargs(self.func.__code__).args 
     return dict(kwargs.items() + zip(spec, args)) 

    def key(self, args, kwargs): 
     a = self.normalize_args(args, kwargs) 
     return tuple(sorted(a.items())) 

例子:

@key_memoized 
def foo(bar, baz, spam): 
    print 'calling foo: bar=%r baz=%r spam=%r' % (bar, baz, spam) 
    return bar + baz + spam 

print foo(1, 2, 3) 
print foo(1, 2, spam=3)   #memoized 
print foo(spam=3, baz=2, bar=1) #memoized 

注您還可以擴展key_memoized並覆蓋其key()方法以提供更具體的記憶策略,例如,忽略一些參數:

class memoize_by_bar(key_memoized): 
    def key(self, args, kwargs): 
     return self.normalize_args(args, kwargs)['bar'] 

@memoize_by_bar 
def foo(bar, baz, spam): 
    print 'calling foo: bar=%r baz=%r spam=%r' % (bar, baz, spam) 
    return bar 

print foo('x', 'ignore1', 'ignore2') 
print foo('x', 'ignore3', 'ignore4') 
+0

我喜歡這樣的外觀,但我關注'key'返回'元組(a.items())'的部分。是否保證按照相同的順序對不同但相同的字典進行排序?我聽說dict是無序的,並且依靠像'str({1:2,3:4})'這樣的東西來產生可重複的字符串,只要給出相同的輸入即爲非常錯誤。 – robru

+0

似乎'inspect.getargspec(func).args [0]'是我問的具體問題(如何找到第一個參數的名稱)的確切答案,但我想將其擴展爲更一般的解決方案稍後我會花一些時間來細化我的結果。 – robru

+0

@Robru:關於字典排序的好處。改成'tuple(sorted(a.items()))'(另一個選項是'frozenset(a.items())')。 – georg

3

嘗試lru_cache

@functools.lru_cache(maxsize=128, typed=False)

裝飾包裹的功能與memoizing調用,節省高達MAXSIZE最近通話。當使用相同的參數週期性地調用昂貴的或I/O綁定函數時,它可以節省時間。添加在Python 3.2

lru_cache,但可移植入2.x的

+0

有趣的閱讀有關,但不幸的是因爲我有需要能夠遍歷高速緩存的實例類staticmethods並不在我的情況下工作,所以緩存本身需要被暴露爲類屬性。 – robru

相關問題