2010-07-08 73 views
3

我有一個方法,它有兩個參數來執行一些複雜的計算。它經常以相同的參數被調用,所以我使用字典進行緩存。目前,這看起來是這樣的:在Python中使用兩個參數緩存函數的結果

def foo(self, a, b): 
    params = frozenset([a, b]) 
    if not params in self._cache: 
     self._cache[params] = self._calculate(a, b) 
    return self._cache[params] 

之所以建立frozenset是該參數可以按任意順序排列,但結果是一樣的。我想知道是否有更簡單的(也是最重要的更高效)解決方案。

回答

0

您的代碼有兩個問題:

(1)使用舊dict.has_key()方法,該方法是緩慢的,已經消失在Python 3.x的而是使用「鍵入字典」或「鍵不在字典中」。

所以:

def foo(self, a, b): 
    params = frozenset([a, b]) 
    if params in self._cache: 
     self._cache[params] = self._calculate(a, b) 
    return self._cache[params] 

(2)「在字典鍵」是更具可讀性,且暴露出太多糟糕的問題:你的代碼不能正常工作!如果參數在字典中,則重新計算。如果他們不在字典中,它將炸燬一個KeyError。考慮複製/粘貼而不是從內存中打字。

所以:

def foo(self, a, b): 
    params = frozenset([a, b]) 
    if params not in self._cache: 
     self._cache[params] = self._calculate(a, b) 
    return self._cache[params] 

(3)一些更效率的建議:

def foo(self, a, b): 
    if a < b: 
     params = (a, b) 
    else: 
     params = (b, a) 
    try: 
     return self._cache[params] 
    except KeyError: 
     v = self._cache[params] = self._calculate(a, b) 
     return v 
+0

只有當他預計超過99%的時間才能訪問緩存時,try塊的效率纔會更高。 http://stackoverflow.com/questions/3111195/python-performance-try-except-or-not-in – Wilduck 2010-07-08 21:29:31

+0

@Wilduck:該基準沒有考慮緩存環境中的_calculate方法花費的時間,其中很可能會使「不在/嘗試/獲得」問題變得相當微不足道。指出了實際應用的基準。 – 2010-07-08 22:18:25

+0

你說得對。我所有的基準都表明,在這裏的任何答案中提出的任何類型的調整都沒有任何可衡量的性能影響(雖然我實現的基本緩存給小數據集帶來了x3性能提升)。 – 2010-07-09 08:12:53

0

您可以將這兩個值合併爲一個散列值,如str(a)+'\ 0'+ str(b),然後將其放入緩存中,也可以製作二維數組,以便緩存[a] [b]返回你想要的值。

您可能還想將此功能轉換爲@decorator類型的函數,然後您可以在多個函數上重複使用裝飾器,併爲緩存維護一個代碼位置。

+0

我喜歡和裝飾的想法。但是使用組合散列的那個不太好,因爲它會導致'a,b'和'b,a'的不同散列,或者我在這裏丟失了什麼。 – 2010-07-08 19:51:20

+0

@cowboy:如果它是對稱的,你可以填充緩存[a] [b]和緩存[b] [a] – eruciform 2010-07-08 20:27:34

0

我只是將它存儲在緩存中兩次,每次訂購一次。

def foo(self, a, b): 
    try: 
     return self._cache[(a, b)] 
    except KeyError: 
     value = self._calculate(a, b) 
     self._cache[(a, b)] = self._cache[(b, a)] = value 
     return value 
0

你可以使用beaker.cache爲(http://beaker.groovie.org/index.html

# define a cache manager 
cache = CacheManager(dict_of_config_options) 

# in your class, apply a cache decorator 
@cache.cache('mycache') 
def foo(self, a,b): 
    return self._calculate 

我覺得它就像你想用默認值。不知道這是否使用自我作爲關鍵的一部分。它假設a,b是pickleable。

+1

謝謝,我全部使用庫來代替重新發明輪子,但在這種情況下,我相信它太過分了。 – 2010-07-08 20:18:34

2

關於如何實現緩存,沒有什麼特別低效或複雜的;這基本上是需要發生的事情。但是,這並不是很通常的

爲方便起見,您可以實現某種更廣義的緩存策略,如果喜歡,可以使用裝飾器。一個可行的方法可能是:

class Memoizer(object): 
    def __init__(self): 
     self._cache = dict() 

    def memoize_unordered(self, f): 
     def wrapper(s, *args, **kwargs): 
      key = (s, f, frozenset(args), frozenset(kwargs.iteritems())) 
      if key not in self._cache: 
       print 'calculating', args, kwargs 
       self._cache[key] = f(s, *args, **kwargs) 
      return self._cache[key] 
     return wrapper 

    def memoize_ordered(self, f): 
     def wrapper(s, *args, **kwargs): 
      key = (s, f, tuple(args), frozenset(kwargs.iteritems())) 
      if key not in self._cache: 
       print 'calculating', args, kwargs 
       self._cache[key] = f(s, *args, **kwargs) 
      return self._cache[key] 
     return wrapper 

memoizer = Memoizer() 

class Foo(object): 

    @memoizer.memoize_unordered 
    def foo(self, a, b): 
     return self._calculate(a, b) 

    def _calculate(self, a, b): 
     return frozenset([a,b]) 

foo = Foo() 


results = [foo.foo(*a) for a in [(1, 5), (1, 5), (5, 1), (9, 12), (12, 9)]] 
for result in results: 
    print 'RESULT', result 

印刷:

calculating (1, 5) {} 
calculating (9, 12) {} 
RESULT frozenset([1, 5]) 
RESULT frozenset([1, 5]) 
RESULT frozenset([1, 5]) 
RESULT frozenset([9, 12]) 
RESULT frozenset([9, 12]) 

當然缺點,你的對象之外實現緩存,就是當你的對象消失緩存中的數據沒有被刪除,除非你謹慎地做到這一點。

+0

+1爲裝飾。 :) – iamgopal 2010-07-09 07:49:50

+0

weakref.WeakKeyDictionary和weakref.WeakValueDictionary是爲了解決「對象永遠因爲在記憶緩存中引用而存在」問題。 – PaulMcG 2010-07-09 09:20:16