带有实例方法的Python函数库lru_cache:释放对象



如何在类内部使用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()

但是foo和因此foo.big(一个BigClass)仍然是活的

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_cache的方法是什么?

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

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

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

简单包装器解决方案

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

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增加了一些开销,因此只有当实例很大并且应用程序无法等待旧的未使用调用从缓存中老化时,才需要使用它。

为什么这样更好

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

处理可变属性

如果方法中使用的任何属性都是可变的,请确保添加_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)

我将为这个用例介绍methodtools

要安装的pip install methodtoolshttps://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。

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

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

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

如果你不想更新到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
    ...

此方法的问题是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

您可以将方法的实现移动到模块全局函数,在从方法调用self时只传递相关数据,并在函数上使用@lru_cache

这种方法的另一个好处是,即使类是可变的,缓存也是正确的。缓存密钥更明确,因为只有相关数据在缓存函数的签名中。

为了使示例稍微更现实一点,让我们假设cached_method()需要来自self.big:的信息

from dataclasses import dataclass
from functools import lru_cache
@dataclass
class BigClass:
    base: int
class Foo:
    def __init__(self):
        self.big = BigClass(base=100)
    @lru_cache(maxsize=16)  # the leak is here
    def cached_method(self, x: int) -> int:
        return self.big.base + x
def fun():
    foo = Foo()
    print(foo.cached_method(10))
    print(foo.cached_method(10)) # use cache
    return 'something'
fun()

现在将实现转移到类之外:

from dataclasses import dataclass
from functools import lru_cache
@dataclass
class BigClass:
    base: int
@lru_cache(maxsize=16)  # no leak from here
def _cached_method(base: int, x: int) -> int:
    return base + x
class Foo:
    def __init__(self):
        self.big = BigClass(base=100)
    def cached_method(self, x: int) -> int:
        return _cached_method(self.big.base, x)
def fun():
    foo = Foo()
    print(foo.cached_method(10))
    print(foo.cached_method(10)) # use cache
    return 'something'
fun()

解决方案

下面是lru_cache的一个小的替换(和包装),它将LRU缓存放在实例(对象)上,而不是放在类上。

摘要

更换件结合了lru_cachecached_property。它使用cached_property在第一次访问时将缓存的方法存储在实例上;以这种方式,CCD_ 32跟随对象,并且作为奖励,它可以用于像未冻结的CCD_。

如何使用

@instance_lru_cache而不是@lru_cache来装饰一种方法,你就做好了。支持Decorator参数,例如@instance_lru_cache(maxsize=None)

与其他答案的比较

结果与pabloi和akaihola提供的答案相当,但使用了简单的decorator语法。和youknowone提供的答案相比,这个decorator是类型提示的,不需要第三方库(结果是可比较的)。

这个答案与Raymond Hettinger提供的答案不同,因为缓存现在存储在实例上(这意味着最大大小是按实例而不是按类定义的),并且它适用于不可处理对象的方法。

from functools import cached_property, lru_cache, partial, update_wrapper
from typing import Callable, Optional, TypeVar, Union
T = TypeVar("T") 
def instance_lru_cache(
    method: Optional[Callable[..., T]] = None,
    *,
    maxsize: Optional[int] = 128,
    typed: bool = False
) -> Union[Callable[..., T], Callable[[Callable[..., T]], Callable[..., T]]]:
    """Least-recently-used cache decorator for instance methods.
    The cache follows the lifetime of an object (it is stored on the object,
    not on the class) and can be used on unhashable objects. Wrapper around
    functools.lru_cache.
    If *maxsize* is set to None, the LRU features are disabled and the cache
    can grow without bound.
    If *typed* is True, arguments of different types will be cached separately.
    For example, f(3.0) and f(3) will be treated as distinct calls with
    distinct results.
    Arguments to the cached method (other than 'self') must be hashable.
    View the cache statistics named tuple (hits, misses, maxsize, currsize)
    with f.cache_info().  Clear the cache and statistics with f.cache_clear().
    Access the underlying function with f.__wrapped__.
    """
    def decorator(wrapped: Callable[..., T]) -> Callable[..., T]:
        def wrapper(self: object) -> Callable[..., T]:
            return lru_cache(maxsize=maxsize, typed=typed)(
                update_wrapper(partial(wrapped, self), wrapped)
            )
        return cached_property(wrapper)  # type: ignore
    return decorator if method is None else decorator(method)

在实例方法上使用@lru_cache或@cache的问题是,self被传递给该方法进行缓存,尽管实际上并不需要。我不能告诉你为什么缓存本身会导致这个问题,但我可以给你一个我认为非常优雅的解决方案。

我喜欢的处理方法是定义一个dunder方法,它是一个类方法,除了self之外,它接受与实例方法相同的所有参数。这是我喜欢的方式,因为它非常清晰,极简主义,不依赖于外部库。

from functools import lru_cache
class BigClass:
    pass

class Foo:
    def __init__(self):
        self.big = BigClass()
    
    @staticmethod
    @lru_cache(maxsize=16)
    def __cached_method__(x: int) -> int:
        return x + 5
    def cached_method(self, x: int) -> int:
        return self.__cached_method__(x)

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

我已经验证了该项目是正确的垃圾收集:

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

相关内容

  • 没有找到相关文章

最新更新