【问题标题】:Is there a decorator to simply cache function return values?是否有一个装饰器可以简单地缓存函数返回值?
【发布时间】:2021-07-19 19:18:33
【问题描述】:

考虑以下几点:

@property
def name(self):

    if not hasattr(self, '_name'):

        # expensive calculation
        self._name = 1 + 1

    return self._name

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

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

【问题讨论】:

  • 可能有一个装饰器具有类似的功能,但你还没有完全指定你想要什么。您使用的是哪种缓存后端?以及如何键入值?我从您的代码中假设您真正要求的是缓存的只读属性。
  • 有 memoizing 装饰器执行你所谓的“缓存”;他们通常处理这样的函数(无论是否打算成为方法),其结果取决于他们的论点(而不是可变的事物,例如 self!-),因此保留一个单独的备忘录字典。

标签: python caching decorator memoization


【解决方案1】:

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

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

装饰器用一个可保存最多 maxsize 最近调用的记忆可调用函数来包装函数。当使用相同的参数定期调用昂贵的或 I/O 绑定的函数时,它可以节省时间。

计算Fibonacci numbers的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 卡住了,这里列出了其他兼容的记忆库:

【讨论】:

  • 现在可以在这里找到反向端口:pypi.python.org/pypi/backports.functools_lru_cache
  • @gerrit 理论上它通常适用于可散列对象 - 尽管某些可散列对象仅在它们是相同对象时才相等(例如没有显式 __hash__() 函数的用户定义对象)。跨度>
  • @Jonathan 它有效,但错误。如果我传递一个可散列的可变参数,并在第一次调用函数后更改对象的值,那么第二次调用将返回更改后的对象,而不是原始对象。这几乎肯定不是用户想要的。要使其适用于可变参数,需要lru_cache 对其缓存的任何结果进行复制,并且在functools.lru_cache 实现中没有进行此类复制。这样做还可能会在用于缓存大型对象时产生难以发现的内存问题。
  • @gerrit 你介意在这里跟进:stackoverflow.com/questions/44583381/… 吗?我没有完全按照你的榜样。
【解决方案2】:

试试joblib https://joblib.readthedocs.io/en/latest/memory.html

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

【讨论】:

    【解决方案3】:

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

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

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

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

    这是一个更漂亮的版本:

    cache = lru_cache(maxsize=None)
    
    @cache
    def func(param1):
       pass
    

    【讨论】:

      【解决方案4】:

      函数缓存简单解决方案

      使用 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
      

      【讨论】:

      • 添加 TTL 是个好主意。但是,如果由于for key in function.cache.keys() 操作而设置了max_iters,则时间复杂度为O(max_iters)。您可能会想到一种在请求(惰性)或字典已满时删除过期项目的方法(删除字典中的第一个。字典在 Python 3.7+ 中保持插入顺序。您可以将 OrderedDict 用于旧版本)
      【解决方案5】:

      @lru_cache 不适合默认属性

      我的@mem装饰器:

      import inspect
      from copy import deepcopy
      from functools import lru_cache, wraps
      from typing import Any, Callable, Dict, Iterable
      
      
      # helper
      def get_all_kwargs_values(f: Callable, kwargs: Dict[str, Any]) -> Iterable[Any]:
          default_kwargs = {
              k: v.default
              for k, v in inspect.signature(f).parameters.items()
              if v.default is not inspect.Parameter.empty
          }
      
          all_kwargs = deepcopy(default_kwargs)
          all_kwargs.update(kwargs)
      
          for key in sorted(all_kwargs.keys()):
              yield all_kwargs[key]
      
      
      # the best decorator
      def mem(func: Callable) -> Callable:
          cache = dict()
      
          @wraps(func)
          def wrapper(*args, **kwargs) -> Any:
              all_kwargs_values = get_all_kwargs_values(func, kwargs)
              params = (*args, *all_kwargs_values)
              _hash = hash(params)
      
              if _hash not in cache:
                  cache[_hash] = func(*args, **kwargs)
      
              return cache[_hash]
      
          return wrapper
      
      
      # some logic
      def counter(*args) -> int:
          print(f'* not_cached:', end='\t')
          return sum(args)
      
      
      @mem
      def check_mem(a, *args, z=10) -> int:
          return counter(a, *args, z)
      
      
      @lru_cache
      def check_lru(a, *args, z=10) -> int:
          return counter(a, *args, z)
      
      
      def test(func) -> None:
          print(f'\nTest {func.__name__}:')
      
          print('*', func(1, 2, 3, 4, 5))
          print('*', func(1, 2, 3, 4, 5))
          print('*', func(1, 2, 3, 4, 5, z=6))
          print('*', func(1, 2, 3, 4, 5, z=6))
          print('*', func(1))
          print('*', func(1, z=10))
      
      
      def main():
          test(check_mem)
          test(check_lru)
      
      
      if __name__ == '__main__':
          main()
      

      输出:

      Test check_mem:
      * not_cached:   * 25
      * 25
      * not_cached:   * 21
      * 21
      * not_cached:   * 11
      * 11
      
      Test check_lru:
      * not_cached:   * 25
      * 25
      * not_cached:   * 21
      * 21
      * not_cached:   * 11
      * not_cached:   * 11
      

      【讨论】:

        【解决方案6】:

        Python 3.8 functools.cached_property 装饰器

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

        来自 Werkzeug 的cached_property 在: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 属性是类命名空间的只读代理),以及那些指定 slots 不包括 dict 作为定义的插槽之一(因为这些类根本不提供 dict 属性)。

        【讨论】:

          【解决方案7】:

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

          chosen answer,只是导入不同:

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

          另外,它安装在Anaconda,不像functools,needs to be installed

          【讨论】:

          • functools 是标准库的一部分,您发布的链接是随机 git fork 或其他东西...
          【解决方案8】:

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

          你应该检查kids.cache,它提供了一个在python 2和python 3上工作的@cache装饰器。没有依赖关系,大约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)) 来装饰您的函数/属性/类/方法...)

          【讨论】:

          【解决方案9】:

          如果您使用的是 Django 框架,它有这样一个属性来缓存 API 的视图或响应 使用@cache_page(time),也可以有其他选项。

          例子:

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

          更多详情请见here

          【讨论】:

            【解决方案10】:

            如果您使用 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
            

            【讨论】:

              【解决方案11】:

              除了Memoize Example,我还发现了以下python包:

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

              【讨论】:

                【解决方案12】:

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

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

                【讨论】:

                • 您的第一个 @cached 缺少括号。否则它只会返回 cached 对象而不是 myfunc 并且当被称为 myfunc() 时,inner 将始终作为返回值返回
                【解决方案13】:
                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'}
                

                【讨论】:

                • 奇怪!这是如何运作的?它看起来不像我见过的其他装饰器。
                • 如果使用关键字参数,此解决方案将返回 TypeError,例如foo(3, b=5)
                • 解决方案的问题在于它没有内存限制。至于命名参数,您可以将它们添加到 __ call__ 和 __ missing__ 像 **nargs
                • 这似乎不适用于类函数,因为在 __missing__: missing 1 required positional argument: 'self' 中引发了 TypeError
                【解决方案14】:

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

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

                我看到的唯一问题是它不适用于大型输入,因为它对 str(arg) 进行哈希处理,这对于巨型数组来说并不是唯一的。

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

                【讨论】:

                  【解决方案15】:

                  Werkzeug 有一个 cached_property 装饰器(docssource

                  【讨论】:

                  • 是的。这值得与一般的 memoization 案例区分开来,因为如果 class 不可散列,则标准 memoization 不起作用。
                  • 现在在 Python 3.8 中:docs.python.org/dev/library/…
                  【解决方案16】:

                  Python Wiki 上还有另一个 memoize 装饰器示例:

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

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

                  【讨论】:

                    【解决方案17】:

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

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

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

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

                    我认为方法调用语法是更好的风格,因为它暗示了计算成本高昂的可能性,而属性语法则暗示了快速查找。

                    [更新:我之前在这里链接和引用的基于类的记忆装饰器不适用于方法。我已将其替换为装饰器函数。] 如果您愿意使用通用的 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)
                    

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

                    【讨论】:

                    • 所有答案中提到的装饰器都不适用于方法!可能是因为它们是基于类的。只有一个自我通过?其他的工作正常,但在函数中存储值很麻烦。
                    • 我认为如果 args 不可散列,您可能会遇到问题。
                    • @Unknown 是的,我在这里引用的第一个装饰器仅限于可散列类型。 ActiveState 中的那个(具有缓存大小限制)将参数腌制为(可散列的)字符串,这当然更昂贵但更通用。
                    • @vanity 感谢您指出基于类的装饰器的局限性。我已经修改了我的答案以显示一个装饰器函数,它适用于方法(我实际上测试了这个)。
                    • @SiminJie 装饰器只被调用一次,它返回的包装函数与对fibonacci 的所有不同调用相同。该函数始终使用相同的 memo 字典。
                    【解决方案18】:

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

                    我也经常这样做;也许我会在我的代码中使用那个配方。

                    【讨论】:

                      猜你喜欢
                      • 2011-01-11
                      • 2022-01-20
                      • 1970-01-01
                      • 2021-04-19
                      • 2020-06-22
                      • 1970-01-01
                      • 2020-06-01
                      • 2020-12-04
                      相关资源
                      最近更新 更多