【问题标题】:Python functools lru_cache with instance methods: release objectPython functools lru_cache 与实例方法:释放对象
【发布时间】:2016-02-13 20:01:49
【问题描述】:

如何在类内部使用functools.lru_cache 而不泄漏内存?

在下面的最小示例中,foo 实例将不会被释放,尽管它​​超出了范围并且没有引用者(lru_cache 除外)。

from functools import lru_cache
class BigClass:
    pass
class Foo:
    def __init__(self):
        self.big = BigClass()
    @lru_cache(maxsize=16)
    def cached_method(self, x):
        return x + 5

def fun():
    foo = Foo()
    print(foo.cached_method(10))
    print(foo.cached_method(10)) # use cache
    return 'something'

fun()

但是foofoo.bigBigClass)仍然存在

import gc; gc.collect()  # collect garbage
len([obj for obj in gc.get_objects() if isinstance(obj, Foo)]) # is 1

这意味着Foo/BigClass 实例仍驻留在内存中。即使删除Foo (del Foo) 也不会释放它们。

为什么lru_cache 会保留实例?缓存不是使用一些哈希而不是实际对象吗?

在类中使用lru_caches 的推荐方式是什么?

我知道两种解决方法: Use per instance cachesmake the cache ignore object(虽然可能会导致错误的结果)

【问题讨论】:

    标签: python caching python-decorators lru functools


    【解决方案1】:

    这不是最干净的解决方案,但对程序员来说是完全透明的:

    import functools
    import weakref
    
    def memoized_method(*lru_args, **lru_kwargs):
        def decorator(func):
            @functools.wraps(func)
            def wrapped_func(self, *args, **kwargs):
                # We're storing the wrapped method inside the instance. If we had
                # a strong reference to self the instance would never die.
                self_weak = weakref.ref(self)
                @functools.wraps(func)
                @functools.lru_cache(*lru_args, **lru_kwargs)
                def cached_method(*args, **kwargs):
                    return func(self_weak(), *args, **kwargs)
                setattr(self, func.__name__, cached_method)
                return cached_method(*args, **kwargs)
            return wrapped_func
        return decorator
    

    它采用与lru_cache 完全相同的参数,并且工作方式完全相同。但是它永远不会将self 传递给lru_cache,而是使用每个实例lru_cache

    【讨论】:

    • 这有点奇怪,实例上的函数仅在第一次调用时被缓存包装器替换。此外,缓存包装函数没有使用lru_cachecache_clear/cache_info 函数(实现这是我首先遇到的)。
    • 这似乎不适用于__getitem__。任何想法为什么?如果你调用 instance.__getitem__(key) 而不是 instance[key],它确实有效。
    • 这不适用于任何特殊方法,因为这些方法是在类槽而不是在实例字典中查找的。设置obj.__getitem__ = lambda item: item 不会导致obj[key] 工作的原因相同。
    • 知道如何让它在 3.x 上工作吗?我收到 TypeError: wrapped_func() missing 1 required positional argument: 'self'
    【解决方案2】:

    我将为这个用例介绍methodtools

    pip install methodtools 安装https://pypi.org/project/methodtools/

    然后您的代码只需将 functools 替换为 methodtools 即可工作。

    from methodtools import lru_cache
    class Foo:
        @lru_cache(maxsize=16)
        def cached_method(self, x):
            return x + 5
    

    当然 gc 测试也返回 0。

    【讨论】:

    • 你可以使用任何一个。 methodtools.lru_cache 的行为与 functools.lru_cache 完全相同,通过在内部重用 functools.lru_cachering.lru 通过在 python 中重新实现 lru 存储来建议更多功能。
    • methodtools.lru_cache 在方法上为类的每个实例使用单独的存储空间,而 ring.lru 的存储空间由类的所有实例共享。
    【解决方案3】:

    python 3.8 在functools 模块中引入了cached_property 装饰器。 经过测试,它似乎没有保留实例。

    如果您不想更新到 python 3.8,可以使用source code。 您只需要导入RLock 并创建_NOT_FOUND 对象。意思:

    from threading import RLock
    
    _NOT_FOUND = object()
    
    class cached_property:
        # https://github.com/python/cpython/blob/v3.8.0/Lib/functools.py#L930
        ...
    

    【讨论】:

      【解决方案4】:

      简单的包装解决方案

      这是一个包装器,它将保留对实例的弱引用:

      import functools
      import weakref
      
      def weak_lru(maxsize=128, typed=False):
          'LRU Cache decorator that keeps a weak reference to "self"'
          def wrapper(func):
      
              @functools.lru_cache(maxsize, typed)
              def _func(_self, *args, **kwargs):
                  return func(_self(), *args, **kwargs)
      
              @functools.wraps(func)
              def inner(self, *args, **kwargs):
                  return _func(weakref.ref(self), *args, **kwargs)
      
              return inner
      
          return wrapper
      

      示例

      像这样使用它:

      class Weather:
          "Lookup weather information on a government website"
      
          def __init__(self, station_id):
              self.station_id = station_id
      
          @weak_lru(maxsize=10)
          def climate(self, category='average_temperature'):
              print('Simulating a slow method call!')
              return self.station_id + category
      

      什么时候使用

      由于weakrefs 增加了一些开销,您只想在实例很大并且应用程序无法等待较旧的未使用调用从缓存中老化时才使用它。

      为什么这样更好

      与其他答案不同,我们只有一个类缓存,而不是每个实例一个缓存。如果您想从最近最少使用的算法中获得一些好处,这一点很重要。使用每个方法的单个缓存,您可以设置 maxsize 以便总内存使用量是有界的,而与活动的实例数量无关。

      处理可变属性

      如果方法中使用的任何属性是可变的,请务必添加 _eq_()_hash_() 方法:

      class Weather:
          "Lookup weather information on a government website"
      
          def __init__(self, station_id):
              self.station_id = station_id
      
          def update_station(station_id):
              self.station_id = station_id
      
          def __eq__(self, other):
              return self.station_id == other.station_id
      
          def __hash__(self):
              return hash(self.station_id)
      

      【讨论】:

      • 很好的答案@Raymond!希望我能给你更多的支持:-)
      【解决方案5】:

      解决这个问题的一个更简单的方法是在构造函数中而不是在类定义中声明缓存:

      from functools import lru_cache
      import gc
      
      class BigClass:
          pass
      class Foo:
          def __init__(self):
              self.big = BigClass()
              self.cached_method = lru_cache(maxsize=16)(self.cached_method)
          def cached_method(self, x):
              return x + 5
      
      def fun():
          foo = Foo()
          print(foo.cached_method(10))
          print(foo.cached_method(10)) # use cache
          return 'something'
          
      if __name__ == '__main__':
          fun()
          gc.collect()  # collect garbage
          print(len([obj for obj in gc.get_objects() if isinstance(obj, Foo)]))  # is 0
      

      【讨论】:

      • 任何解释为什么这个案例有效而问题中的那个无效?
      • 这个版本的缓存是类实例的本地缓存,因此当实例被删除时缓存也是如此。如果你想要一个全局缓存,那么它在内存中是有弹性的
      猜你喜欢
      • 2020-12-01
      • 1970-01-01
      • 2018-09-27
      • 2021-12-17
      • 1970-01-01
      • 1970-01-01
      • 2016-12-20
      • 1970-01-01
      相关资源
      最近更新 更多