Python functools lru_cache с методами класса: объект выпуска

Как я могу использовать lru_cache functools внутри классов без утечки памяти? В следующем минимальном примере экземпляр 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 (a 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_caches внутри классов?

Я знаю два обходных пути: использовать кеши для каждого экземпляра или заставить кеш игнорировать объект (хотя это может привести к неверным результатам)


person televator    schedule 12.11.2015    source источник


Ответы (5)


Это не самое чистое решение, но оно полностью прозрачно для программиста:

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 для каждого экземпляра.

person orlp    schedule 12.11.2015
comment
В этом есть небольшая странность, заключающаяся в том, что функция в экземпляре заменяется кэширующей оболочкой только при первом вызове. Кроме того, функция-оболочка кэширования не помазана функциями lru_cache _2 _ / _ 3_ (реализация которых была там, где я впервые столкнулся с этим). - person AKX; 13.11.2018
comment
Похоже, это не работает для __getitem__. Есть идеи, почему? Он работает, если вы вызываете instance.__getitem__(key), но не instance[key]. - person JoseKilo; 07.08.2019
comment
Это не сработает для какого-либо специального метода, потому что они ищутся в слотах классов, а не в словарях экземпляров. По той же причине, по которой установка obj.__getitem__ = lambda item: item не приведет к работе obj[key]. - person pankaj; 06.11.2020

Я представлю 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.

person youknowone    schedule 05.05.2019
comment
Вы можете использовать любой из них. methodtools.lru_cache ведет себя точно так же, как functools.lru_cache, повторно используя functools.lru_cache внутри, в то время как ring.lru предлагает больше функций, переопределяя хранилище lru в python. - person youknowone; 05.06.2019
comment
methodtools.lru_cache в методе использует отдельное хранилище для каждого экземпляра класса, в то время как хранилище ring.lru используется всеми экземплярами класса. - person Filip Bártek; 14.08.2019

python 3.8 представил декоратор cached_property в модуле functools. при тестировании кажется, что экземпляры не сохраняются.

Если вы не хотите обновляться до 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
    ...
person moshevi    schedule 28.10.2019

Простое упаковочное решение

Вот оболочка, которая будет хранить слабую ссылку на экземпляр:

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)
person Raymond Hettinger    schedule 20.06.2021

Еще более простое решение этой проблемы - объявить кеш в конструкторе, а не в определении класса:

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
person pabloi    schedule 27.07.2021