在Python中使用两个参数缓存函数的结果

问题描述:

我有一个方法,它有两个参数来执行一些复杂的计算。它经常以相同的参数被调用,所以我使用字典进行缓存。目前,这看起来是这样的:在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是该参数可以按任意顺序排列,但结果是一样的。我想知道是否有更简单的(也是最重要的更高效)解决方案。

您的代码有两个问题:

(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

您可以将这两个值合并为一个散列值,如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

我只是将它存储在缓存中两次,每次订购一次。

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 

你可以使用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

关于如何实现缓存,没有什么特别低效或复杂的;这基本上是需要发生的事情。但是,这并不是很通常的

为方便起见,您可以实现某种更广义的缓存策略,如果喜欢,可以使用装饰器。一个可行的方法可能是:

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