Python functools lru_cache与类方法:释放对象

tel*_*tor 33 python caching lru functools python-decorators

如何在不泄漏内存的情况下在类中使用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()
Run Code Online (Sandbox Code Playgroud)

但是foo因此foo.big(a BigClass)仍然活着

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

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

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

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

我知道两个解决方法: 使用每个实例缓存使缓存忽略对象(这可能会导致错误的结果)

orl*_*rlp 30

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

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
Run Code Online (Sandbox Code Playgroud)

它采用完全相同的参数lru_cache,并且工作原理完全相同.但是它永远不会传递selflru_cache而是使用每个实例lru_cache.

  • 这有一点奇怪,即实例上的函数仅在第一次调用时被缓存包装器替换。此外,缓存包装器函数没有涂上`lru_cache` 的`cache_clear`/`cache_info` 函数(实现这是我首先遇到的)。 (2认同)

Ray*_*ger 30

简单的包装解决方案

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

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
Run Code Online (Sandbox Code Playgroud)

例子

像这样使用它:

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
Run Code Online (Sandbox Code Playgroud)

何时使用它

由于弱引用会增加一些开销,因此您只想在实例很大并且应用程序无法等待较旧的未使用调用从缓存中过期时才使用它。

为什么这个更好

与其他答案不同,我们只有一个类缓存,而不是每个实例都有一个缓存。如果您想从最近最少使用的算法中获得一些好处,这一点很重要。通过每个方法一个缓存,您可以设置 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)
Run Code Online (Sandbox Code Playgroud)

  • 很好的答案@Raymond!希望我能给你更多的赞成票:-) (3认同)

pab*_*loi 22

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

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
Run Code Online (Sandbox Code Playgroud)

  • 在这个版本中,缓存是类实例的本地缓存,因此当实例被删除时,缓存也会被删除。如果你想要一个全局缓存,那么它在内存中是有弹性的 (3认同)

you*_*one 18

我将介绍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
Run Code Online (Sandbox Code Playgroud)

当然 gc 测试也返回 0。

  • 方法上的“methodtools.lru_cache”为类的每个实例使用单独的存储,而“ring.lru”的存储由类的所有实例共享。 (4认同)
  • 您可以使用任何一种。`methodtools.lru_cache` 的行为与 `functools.lru_cache` 完全一样,通过在内部重用 `functools.lru_cache` 而 `ring.lru` 通过在 python 中重新实现 lru 存储来建议更多功能。 (2认同)

Ray*_*ger 8

此方法的问题在于这self是一个未使用的变量。

简单的解决方案是将方法变成静态方法。这样,该实例就不是缓存的一部分。

class Foo:
    def __init__(self):
        self.big = BigClass()

    @staticmethod                   # <-- Add this line
    @lru_cache(maxsize=16)
    def cached_method(x):
        print('miss')
        return x + 5
Run Code Online (Sandbox Code Playgroud)


mos*_*evi 5

python 3.8cached_propertyfunctools模块中引入了装饰器。测试时,它似乎没有保留实例。

如果您不想更新到 python 3.8,可以使用源代码。您只需要导入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
    ...
Run Code Online (Sandbox Code Playgroud)

  • 在这种情况下,“cached_property”是无用的 - 您不能使用参数(与任何属性一样)。 (8认同)