有没有一个装饰器可以简单地缓存函数的返回值?

256

考虑以下内容:

@property
def name(self):

    if not hasattr(self, '_name'):

        # expensive calculation
        self._name = 1 + 1

    return self._name

我是新手,但我认为缓存可以拆分成一个装饰器。只是我没有找到类似的装饰器;)

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


可能有一种装饰器具有类似的功能,但您还没有充分说明您想要什么。您使用了什么类型的缓存后端?值将如何被键入?从您的代码中我假设您真正需要的是一个缓存的只读属性。 - David Berger
有一些记忆化装饰器可以执行所谓的“缓存”,它们通常适用于函数(无论是否打算成为方法)上,这些函数的结果取决于它们的参数(而不是可变的东西,如self!-),因此保留一个单独的备忘录字典。 - Alex Martelli
20个回答

295

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

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

该装饰器用于将函数包装为一个记忆可调用对象,最多保存最近的maxsize个调用。当一个昂贵或I/O绑定的函数被周期性地使用相同的参数调用时,它可以节省时间。

计算斐波那契数列的LRU缓存示例:

from functools import lru_cache

@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中,这里有一些其他兼容的记忆化库列表:

2
回溯 http://code.activestate.com/recipes/578078-py26-and-py30-backport-of-python-33s-lru-cache/ 的 Py26 和 Py30,这是 Python 33 LRU 缓存的回溯。 - Kos
现在可以在此处找到回溯:https://pypi.python.org/pypi/backports.functools_lru_cache - Frederick Nord
@gerrit 理论上它适用于一般可哈希对象 - 尽管有些可哈希对象只有在它们是同一对象时才相等(比如没有明确的 hash() 函数的用户定义对象)。 - Jonathan
2
@Jonathan 它能工作,但是有问题。如果我传递一个可哈希、可变的参数,并在第一次调用函数后更改对象的值,则第二次调用将返回更改后的对象,而不是原始对象。这几乎肯定不是用户想要的。为了使其对可变参数起作用,需要 lru_cache 复制它缓存的任何结果,而在 functools.lru_cache 实现中并没有进行这样的复制。这样做也会有可能在用于缓存大对象时产生难以找到的内存问题。 - gerrit
@gerrit 你能否在这里跟进一下:https://stackoverflow.com/questions/44583381/what-difficulties-might-arise-from-using-mutable-arguments-to-an-lru-cache-dec?我没有完全理解你的例子。 - Jonathan
显示剩余2条评论

58

Python 3.8 functools.cached_property装饰器

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

cached_property是由Werkzeug提供的,它在这里被提到:https://dev59.com/u3RA5IYBdhLWcg3wzhbZ#5295190 ,但一种推测派生版本将会合并到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 包含在定义的 slot 中的类型(此类不提供任何 dict 属性)。


这能与标准库 shelve 结合使用吗?我想要将属性缓存到磁盘上。有类似的东西吗? - Gulzar

51

functools.cache 已在 Python 3.9 中发布 (文档)。

from functools import cache

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

在之前的Python版本中,其中一个早期回答仍然是有效的解决方案:使用lru_cache作为普通缓存,没有限制和LRU特性。(文档

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

这里有一个更漂亮的版本:

cache = lru_cache(maxsize=None)

@cache
def func(param1):
   pass

我已经测试过了,它只会让执行时间变长!此外,第二次、第三次等执行时间都相同... - Yu Da Chi
1
如果您使用相同的参数再次调用相同的函数,将会很有帮助。您确定参数是相同的吗? - Alperen
哦,我现在明白了!!(真遗憾_ for _ in range(1500): factorial(496) 这个花费了0.0941而仅缓存变量时需要0.0003... 我本以为这将被缓存以加速整个文件的迭代,而不仅仅是在一个程序循环期间,唉... - Yu Da Chi

40

听起来你不是在寻求一个通用的记忆化装饰器(即,你对于缓存不同参数值的返回值的一般情况不感兴趣)。也就是说,你想要这样的东西:

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)

在这里可以找到另一个具有缓存大小限制的记忆化装饰器,链接为此处


1
所有答案中提到的装饰器都不适用于方法!可能是因为它们是基于类的。只传递一个self?其他装饰器可以正常工作,但在函数中存储值会使代码变得混乱。 - Tobias
2
如果args不可哈希,我认为你可能会遇到问题。 - Unknown
1
@Unknown 是的,我在这里引用的第一个装饰器只适用于可哈希类型。ActiveState上的那个(带缓存大小限制)会将参数序列化成(可哈希的)字符串,当然更昂贵但更通用。 - Nathan Kitchen
@vanity 感谢您指出基于类的装饰器的限制。我已经修改了我的答案,展示了一个装饰器函数,它适用于方法(我实际上测试过这个)。 - Nathan Kitchen
2
@SiminJie 装饰器只被调用一次,它返回的包装函数是用于所有不同对 fibonacci 的调用的相同函数。该函数始终使用相同的 memo 字典。 - Nathan Kitchen
显示剩余6条评论

27
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
2
如果使用关键字参数,例如foo(3, b=5),则此解决方案会返回TypeError。 - kadee
2
解决方案的问题在于它没有内存限制。至于命名参数,您可以像 **nargs 一样将它们添加到 callmissing 中。 - Leonid Mednikov
这似乎不适用于类函数,因为在__missing__中会引发TypeError错误:缺少1个必需的位置参数:'self' - jl005

12

是的。这很值得区分一般记忆化情况,因为如果类不可哈希,则标准记忆化无法工作。 - Jameson Quinn
1
现在在Python 3.8中:https://docs.python.org/dev/library/functools.html#functools.cached_property - Ciro Santilli OurBigBook.com

10

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

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 将始终作为返回值被返回。 - Markus Meskanen
仅在函数对不同参数返回相同响应时进行缓存。 - cmd

8

免责声明:我是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

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

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

重要提示:默认的kids.cache缓存存储是标准字典,不建议在长时间运行的程序中使用,因为它会导致不断增长的缓存存储。对于此用途,您可以使用其他缓存存储插件,例如(@cache(use=cachetools.LRUCache(maxsize=2))来装饰您的函数/属性/类/方法...)


这个模块在Python 2上似乎导入时间很慢,大约0.9秒(参见:https://pastebin.com/raw/aA1ZBE9Z)。我怀疑是由于这行代码引起的https://github.com/0k/kids.cache/blob/master/src/kids/__init__.py#L3 (参考setuptools入口点)。我正在为此创建一个问题。 - Att Righ
这是上述问题的链接 https://github.com/0k/kids.cache/issues/9。 - Att Righ
这会导致内存泄漏。 - Timothy Zhang
@vaab 创建一个 MyClass 的实例 c,并使用 objgraph.show_backrefs([c], max_depth=10) 进行检查,可以看到从类对象 MyClassc 有一个引用链。也就是说,在 MyClass 被释放之前,c 永远不会被释放。 - Timothy Zhang
@TimothyZhang,欢迎您加入https://github.com/0k/kids.cache/issues/10并提出您的疑虑。Stackoverflow不是进行适当讨论的正确场所。还需要进一步澄清。感谢您的反馈。 - vaab

7

7
啊,我只需要找到这个正确的名称:“延迟属性求值”就可以了。
我也经常使用这种方法;也许我会在我的代码中使用这个配方。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接