是否有一个装饰器来简单地缓存函数返回值?

时间:2009-05-02 16:15:40

标签: python caching decorator

请考虑以下事项:

@property
def name(self):

    if not hasattr(self, '_name'):

        # expensive calculation
        self._name = 1 + 1

    return self._name

我是新手,但我认为缓存可能会被装入装饰器中。只有我没有找到喜欢它的人;)

PS真正的计算不依赖于可变值

18 个答案:

答案 0 :(得分:159)

从Python 3.2开始,有一个内置的装饰器:

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

Decorator用一个memoizing callable来包装一个函数,它可以节省maxsize最近的调用。当使用相同的参数定期调用昂贵的或I / O绑定函数时,它可以节省时间。

用于计算Fibonacci numbers的LRU缓存示例:

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

>>> print([fib(n) for n in range(16)])
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]

>>> print(fib.cache_info())
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)

如果你遇到Python 2.x,这里有一个其他兼容的memoization库列表:

答案 1 :(得分:26)

听起来你要求使用通用的memoization装饰器(即,你不想对你想要为不同的参数值缓存返回值的一般情况感兴趣)。也就是说,你想拥有这个:

x = obj.name  # expensive
y = obj.name  # cheap

虽然通用的memoization装饰器会给你这个:

x = obj.name()  # expensive
y = obj.name()  # cheap

我认为方法调用语法是更好的样式,因为它提示了昂贵计算的可能性,而属性语法建议快速查找。

[更新:我之前链接到并引用的基于类的memoization装饰器不适用于方法。我用装饰器函数替换了它。]如果你愿意使用通用的memoization装饰器,这里有一个简单的:

def memoize(function):
  memo = {}
  def wrapper(*args):
    if args in memo:
      return memo[args]
    else:
      rv = function(*args)
      memo[args] = rv
      return rv
  return wrapper

使用示例:

@memoize
def fibonacci(n):
  if n < 2: return n
  return fibonacci(n - 1) + fibonacci(n - 2)

可以找到另一个对缓存大小有限制的memoization装饰器here

答案 2 :(得分:20)

class memorize(dict):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args):
        return self[args]

    def __missing__(self, key):
        result = self[key] = self.func(*key)
        return result

样本使用:

>>> @memorize
... def foo(a, b):
...     return a * b
>>> foo(2, 4)
8
>>> foo
{(2, 4): 8}
>>> foo('hi', 3)
'hihihi'
>>> foo
{(2, 4): 8, ('hi', 3): 'hihihi'}

答案 3 :(得分:9)

Werkzeug有一个cached_property装饰器(docssource

答案 4 :(得分:7)

我编写了这个简单的装饰器类来缓存函数响应。我发现它对我的项目非常有用:

from datetime import datetime, timedelta 

class cached(object):
    def __init__(self, *args, **kwargs):
        self.cached_function_responses = {}
        self.default_max_age = kwargs.get("default_cache_max_age", timedelta(seconds=0))

    def __call__(self, func):
        def inner(*args, **kwargs):
            max_age = kwargs.get('max_age', self.default_max_age)
            if not max_age or func not in self.cached_function_responses or (datetime.now() - self.cached_function_responses[func]['fetch_time'] > max_age):
                if 'max_age' in kwargs: del kwargs['max_age']
                res = func(*args, **kwargs)
                self.cached_function_responses[func] = {'data': res, 'fetch_time': datetime.now()}
            return self.cached_function_responses[func]['data']
        return inner

用法很简单:

import time

@cached
def myfunc(a):
    print "in func"
    return (a, datetime.now())

@cached(default_max_age = timedelta(seconds=6))
def cacheable_test(a):
    print "in cacheable test: "
    return (a, datetime.now())


print cacheable_test(1,max_age=timedelta(seconds=5))
print cacheable_test(2,max_age=timedelta(seconds=5))
time.sleep(7)
print cacheable_test(3,max_age=timedelta(seconds=5))

答案 5 :(得分:6)

免责声明:我是kids.cache的作者。

你应该检查kids.cache,它提供一个@cache装饰器,可以在python 2和python 3上运行。没有依赖关系,大约100行代码。例如,考虑到您的代码,使用起来非常简单,您可以像这样使用它:

pip install kids.cache

然后

from kids.cache import cache
...
class MyClass(object):
    ...
    @cache            # <-- That's all you need to do
    @property
    def name(self):
        return 1 + 1  # supposedly expensive calculation

或者您可以将@cache装饰器放在@property之后(相同的结果)。

在属性上使用缓存称为 lazy evaluation kids.cache可以做更多的事情(它可以在函数上使用任何参数,属性,任何类型的方法,甚至类。 )。对于高级用户,kids.cache支持cachetools,它为python 2和python 3(LRU,LFU,TTL,RR缓存)提供精美的缓存存储。

重要提示kids.cache的默认缓存存储是一个标准的dict,不推荐用于长期运行的程序,因为它会导致不断增长的缓存存储。对于此用法,您可以使用例如@cache(use=cachetools.LRUCache(maxsize=2))来插入其他缓存存储来装饰您的函数/属性/类/方法...)

答案 6 :(得分:5)

啊,只需找到正确的名称:“Lazy property evaluation”。

我也做了很多;也许我会在我的代码中使用那个食谱。

答案 7 :(得分:3)

如果您使用的是Django Framework,它具有缓存API视图或响应的属性 使用@cache_page(time),也可以有其他选项。

示例:

@cache_page(60 * 15, cache="special_cache")
def my_view(request):
    ...

可以找到更多详细信息here

答案 8 :(得分:3)

在Python Wiki上还有另一个 memoize 装饰器的例子:

http://wiki.python.org/moin/PythonDecoratorLibrary#Memoize

这个例子有点聪明,因为如果参数是可变的,它不会缓存结果。 (检查代码,它非常简单有趣!)

答案 9 :(得分:2)

Memoize Example一起,我找到了以下python包:

  • cachepy;它允许设置ttl和\或缓存函数的调用次数;此外,可以使用加密的基于文件的缓存...
  • percache

答案 10 :(得分:2)

fastcache,它是“ Python 3 functools.lru_cache的C实现。与标准库相比提供了10-30倍的加速。”

chosen answer相同,只是导入不同:

mf

此外,它与Anaconda的functools不同,它安装在needs to be installed中。

答案 11 :(得分:2)

函数缓存简单解决方案

使用 ttl(生存时间)和 max_entries

  • 可选参数:ttl(每个条目的生存时间)
  • 可选参数:max_entries(如果缓存参数组合过多,不会使存储混乱)
  • 确保函数没有重要的副作用

示例使用

import time

@cache(ttl=timedelta(minutes=3), max_entries=300)
def add(a, b):
    time.sleep(2)
    return a + b

@cache()
def substract(a, b):
    time.sleep(2)
    return a - b

a = 5
# function is called with argument combinations the first time -> it takes some time
for i in range(5):
    print(add(a, i))

# function is called with same arguments again? -> will answer from cache
for i in range(5):
    print(add(a, i))

复制装饰器代码

from datetime import datetime, timedelta

def cache(**kwargs):
  def decorator(function):
    # static function variable for cache, lazy initialization
    try: function.cache
    except: function.cache = {}
    def wrapper(*args):
        # if nothing valid in cache, insert something
        if not args in function.cache or datetime.now() > function.cache[args]['expiry']:
            if 'max_entries' in kwargs:
                max_entries = kwargs['max_entries']
                if max_entries != None and len(function.cache) >= max_entries:
                    now = datetime.now()
                    function.cache = { key: function.cache[key] for key in function.cache.keys() if function.cache[key]['expiry'] > now }
                    # if nothing is expired that is deletable, delete the first
                    if len(function.cache) >= max_entries:
                        del function.cache[next(iter(function.cache))]
            function.cache[args] = {'result': function(*args), 'expiry': datetime.max if 'ttl' not in kwargs else datetime.now() + kwargs['ttl']}

        # answer from cache
        return function.cache[args]['result']
    return wrapper
  return decorator

答案 12 :(得分:1)

我实现了类似的东西,使用pickle持久化并使用sha1来获得几乎肯定唯一的ID。基本上缓存散列函数的代码和参数的hist以获取sha1然后在名称中查找具有该sha1的文件。如果它存在,它打开它并返回结果;如果没有,它会调用该函数并保存结果(如果需要花费一定的时间来处理,则可选择保存)。

那就是说,我发誓我找到了一个现有的模块来做这个并发现自己在这里试图找到那个模块......我能找到的最接近的是这个,看起来是正确的:http://chase-seibert.github.io/blog/2011/11/23/pythondjango-disk-based-caching-decorator.html

我看到的唯一问题是它不适用于大型输入,因为它散列了str(arg),这对于巨型数组来说并不是唯一的。

如果有一个 unique_hash ()协议让一个类返回其内容的安全哈希,那就太好了。我基本上手动实现了我关心的类型。

答案 13 :(得分:1)

I A A2 B B2 1 'dog' 10 'cat' 20 2 'elf' 15 'egg' 45 3 'hat' 80 'bag' 50 不适用于默认函数值

我的@lru_cache装饰者:

mem

和测试代码:

import inspect


def get_default_args(f):
    signature = inspect.signature(f)
    return {
        k: v.default
        for k, v in signature.parameters.items()
        if v.default is not inspect.Parameter.empty
    }


def full_kwargs(f, kwargs):
    res = dict(get_default_args(f))
    res.update(kwargs)
    return res


def mem(func):
    cache = dict()

    def wrapper(*args, **kwargs):
        kwargs = full_kwargs(func, kwargs)
        key = list(args)
        key.extend(kwargs.values())
        key = hash(tuple(key))
        if key in cache:
            return cache[key]
        else:
            res = func(*args, **kwargs)
            cache[key] = res
            return res
    return wrapper

结果-睡眠只有3次

但使用from time import sleep @mem def count(a, *x, z=10): sleep(2) x = list(x) x.append(z) x.append(a) return sum(x) def main(): print(count(1,2,3,4,5)) print(count(1,2,3,4,5)) print(count(1,2,3,4,5, z=6)) print(count(1,2,3,4,5, z=6)) print(count(1)) print(count(1, z=10)) if __name__ == '__main__': main() 将是4次,因为:

@lru_cache

将被计算两次(默认设置无效)

答案 14 :(得分:1)

Python 3.8 cached_property装饰器

http://10.10.10.10/

在以下地方提到了来自Werkzeug的

cached_propertyhttps://docs.python.org/dev/library/functools.html#functools.cached_property,但假设派生的版本将被合并到3.8中,这太棒了。

对于没有任何参数的装饰器,可以将其视为缓存@property或清洁器@functools.lru_cache

文档说:

@functools.cached_property(func)
     

将类的方法转换为属性,该属性的值将被计算一次,然后在实例生命周期中作为常规属性进行缓存。与property()类似,但增加了缓存。对于实例的昂贵的计算属性很有用,这些实例原本是有效不变的。

     

示例:

class DataSet:
    def __init__(self, sequence_of_numbers):
        self._data = sequence_of_numbers

    @cached_property
    def stdev(self):
        return statistics.stdev(self._data)

    @cached_property
    def variance(self):
        return statistics.variance(self._data)
     

3.8版的新功能。

     

注意此修饰器要求每个实例上的 dict 属性是可变映射。这意味着它不适用于某些类型,例如元类(因为类型实例上的 dict 属性是类命名空间的只读代理),以及那些指定 slots 的属性。 >而不将 dict 作为已定义的广告位之一(因为此类完全不提供 dict 属性)。

答案 15 :(得分:0)

尝试joblib http://pythonhosted.org/joblib/memory.html

from joblib import Memory
memory = Memory(cachedir=cachedir, verbose=0)
@memory.cache
    def f(x):
        print('Running f(%s)' % x)
        return x

答案 16 :(得分:0)

如果您正在使用Django并希望缓存视图,请参阅Nikhil Kumar's answer


但是如果要缓存任何函数结果,可以使用django-cache-utils

它重用Django缓存并提供易于使用的cached装饰器:

from cache_utils.decorators import cached

@cached(60)
def foo(x, y=0):
    print 'foo is called'
    return x+y

答案 17 :(得分:0)

functools.cache在Python 3.9(docs)中发布:

from functools import cache

@cache
def factorial(n):
    return n * factorial(n-1) if n else 1

在以前的版本中,one of the early answers仍然是有效的解决方案,使用lru_cache作为没有限制和lru功能的普通缓存。 (docs

如果maxsize设置为None,则禁用LRU功能并缓存 可以无限地成长。

这是它的更漂亮的版本:

cache = lru_cache(maxsize=None)

@cache
def func(param1):
   pass