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


157

考虑以下:

@property
def name(self):

    if not hasattr(self, '_name'):

        # expensive calculation
        self._name = 1 + 1

    return self._name

我是新手,但我认为可以将缓存分解为装饰器。只有我找不到喜欢的人;)

PS实际计算不取决于可变值


那里可能有一个具有类似功能的装饰器,但是您尚未完全指定所需的装饰器。您使用哪种缓存后端?以及如何键入值?我从您的代码中假设您真正要的是缓存的只读属性。
David Berger,2009年

有记忆的装饰器执行您所谓的“缓存”。它们通常在这样的函数上工作(无论是否要成为方法),其结果取决于其参数(而不取决于可变的事物,例如self!-),因此保留单独的备忘单。
Alex Martelli的

Answers:


206

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

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

装饰器用备注可调用函数包装一个函数,该函数可保存最多最近调用的最大大小。当使用相同的参数定期调用昂贵的或I / O绑定的函数时,可以节省时间。

用于计算斐波纳契数的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感到困惑,那么这里是其他兼容的备注库的列表:




理论上,@ gerrit通常适用于可哈希对象-尽管某些可哈希对象仅在它们是相同对象时才相等(例如,没有显式__hash __()函数的用户定义对象)。
乔纳森

1
@Jonathan它有效,但是错误。如果我传递一个可哈希的可变参数,并在第一次调用该函数后更改对象的值,则第二次调用将返回更改后的对象,而不是原始对象。几乎可以肯定这不是用户想要的。为了使它适用于可变参数,需要lru_cache将其缓存的任何结果复制一份,并且在functools.lru_cache实现中不会复制任何此类副本。当用于缓存大对象时,这样做还可能会带来难以发现的内存问题。
Gerrit

@gerrit您介意在这里跟进吗:stackoverflow.com/questions/44583381/…?我没有完全听从您的榜样。
乔纳森

28

听起来您好像并没有要求通用的备忘录装饰器(即,您对要为不同的参数值缓存返回值的一般情况不感兴趣)。也就是说,您想要这样:

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

而通用的备忘装饰器会为您提供:

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

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

[更新:我以前链接并在此处引用的基于类的备忘录装饰器不适用于方法。我已经用装饰器函数代替了它。]如果您愿意使用通用的备忘录装饰器,这是一个简单的例子:

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)

这里可以找到另一个限制缓存大小的备忘装饰器。


所有答案中提到的装饰器都无法用于方法!可能是因为它们是基于类的。只有一个人通过了?其他方法工作正常,但是将值存储在函数中很麻烦。
Tobias

2
我认为如果args不可散列,您可能会遇到问题。
未知

1
@Unknown是,我在这里引用的第一个装饰器仅限于可哈希类型。ActiveState中的一个(具有高速缓存大小限制)将参数腌入一个(可哈希)字符串中,这当然更昂贵但更通用。
弥敦道厨房

@vanity感谢您指出基于类的装饰器的局限性。我已经修改了答案,以显示一个装饰器函数,该函数可用于方法(我实际上已经测试了这个方法)。
弥敦道厨房

1
@SiminJie装饰器仅被调用一次,它返回的包装函数与用于的所有不同调用相同fibonacci。该函数始终使用相同的memo字典。
弥敦道厨房

22
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'}

奇怪!这是如何运作的?似乎不像我见过的其他装饰器。
PascalVKooten

1
如果使用关键字参数,则此解决方案将返回TypeError,例如foo(3,b = 5)
kadee

1
解决方案的问题是它没有内存限制。至于命名的参数,您可以将它们添加到__ call__和__ missing__之类的** nargs
Leonid Mednikov,

16

Python 3.8 functools.cached_property装饰器

https://docs.python.org/dev/library/functools.html#functools.cached_property

cached_property在以下网址中提到了来自Werkzeug的文章:https : //stackoverflow.com/a/5295190/895245,但假设派生的版本将合并到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属性是类名称空间的只读代理),以及那些指定而不将dict作为已定义槽之一的类(例如此类)根本不提供dict属性)。



9

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

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))

1
您的第一个@cached缺少括号。否则只会返回cached到位的对象myfunc在调用时,myfunc()inner总是会返回一个返回值
马库斯Meskanen

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(相同结果)之后。

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

重要说明:的默认缓存存储区kids.cache是标准字典,不建议对运行时间长且查询内容不同的长期运行的程序进行存储,因为它会导致缓存存储区的不断增长。对于这种用法,您可以使用例如插入其他缓存存储(@cache(use=cachetools.LRUCache(maxsize=2))以装饰您的功能/属性/类/方法...)


这个模块似乎导致python 2〜0.9s上的导入时间变慢(请参阅:pastebin.com/raw/aA1ZBE9Z)。我怀疑这是由于此行github.com/0k/kids.cache/blob/master/src/kids /__init__. py#L3(参见setuptools入口点)。我为此创建了一个问题。
Att Righ

这是上述github.com/0k/kids.cache/issues/9的问题。
Att Righ

这将导致内存泄漏。
蒂莫西·张

@vaab创建一个实例cMyClass,与检查它objgraph.show_backrefs([c], max_depth=10),有从类对象的Ref链MyClassc。就是说,c直到被释放才被MyClass释放。
蒂莫西·张

欢迎您@TimothyZhang并在github.com/0k/kids.cache/issues/10中添加您的疑虑。Stackoverflow不是进行适当讨论的正确位置。并且需要进一步澄清。感谢您的反馈意见。
Vaab


4

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

选择的答案相同,只是导入不同:

from fastcache import lru_cache
@lru_cache(maxsize=128, typed=False)
def f(a, b):
    pass

此外,它还安装在Anaconda中,与需要安装的 functools不同。


1
functools是标准库的一部分,您发布的链接是指向随机git fork或其他内容的
cz


3

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

例:

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

可以在此处找到更多详细信息。



1

我实现了类似的方法,使用pickle进行持久化,并使用sha1来实现几乎确定的短唯一ID。基本上,缓存会对函数的代码和参数的历史进行哈希处理,以获取sha1,然后查找名称中具有sha1的文件。如果存在,则将其打开并返回结果。如果不是,它将调用该函数并保存结果(可选地,仅在处理了一定时间后才保存)。

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

我唯一看到的问题是,它不能对大型输入有效,因为它会散列str(arg),这对于巨型数组并不是唯一的。

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




1

@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

和测试代码:

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()

结果-睡眠只有3次

但这@lru_cache将是4次,因为:

print(count(1))
print(count(1, z=10))

将被计算两次(默认情况下无效)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.