Python: отзыв кэшированного результата функции, зависящего от нового параметра функции



Я довольно новичок в концепциях кэширования и запоминания. Я читал некоторые другие обсуждения и ресурсы здесь, здесь , и здесь, но не смог проследить их все так хорошо.

Скажем, что у меня есть две функции-члена в классе. (Упрощенный пример ниже.) Предположим, что первая функция total является вычислительно дорогой. Вторая функция subtotal вычислительно проста, за исключением того, что она использует возврат из первой функции, и поэтому также становится вычислительно дорогим из-за этого, в том, что в настоящее время он должен повторно вызвать total, чтобы получить свой возвращенный результат.

Я хочу кэшировать результаты первой функции и использовать их в качестве входных данных для второй, если вход y к subtotal разделяет входные данные x к недавнему вызову total. То есть:

  • если вызов subtotal (), где y равен значению x в
    предыдущий вызов total, а затем использовать этот кэшированный результат вместо из
    повторный вызов total.
  • В противном случае просто вызовите total() с помощью x = y.

Пример:

class MyObject(object):

    def __init__(self, a, b):
        self.a, self.b = a, b

    def total(self, x):
        return (self.a + self.b) * x     # some time-expensive calculation

    def subtotal(self, y, z):
        return self.total(x=y) + z       # Don't want to have to re-run total() here
                                         # IF y == x from a recent call of total(),
                                         # otherwise, call total().
142   3  

3 ответов:

Спасибо всем за ответы, было полезно просто прочитать их и посмотреть, что происходит под капотом. Как сказал @Tadhg McDonald-Jensen, похоже, мне здесь больше ничего не нужно, кроме @functools.lru_cache. (Я в Python 3.5.) Что касается комментария @unutbu, я не получаю ошибку от украшения total () с @lru_cache. Позвольте мне исправить мой собственный пример, я продолжу это здесь для других новичков:

from functools import lru_cache
from datetime import datetime as dt

class MyObject(object):
    def __init__(self, a, b):
        self.a, self.b = a, b

    @lru_cache(maxsize=None)
    def total(self, x):        
        lst = []
        for i in range(int(1e7)):
            val = self.a + self.b + x    # time-expensive loop
            lst.append(val)
        return np.array(lst)     

    def subtotal(self, y, z):
        return self.total(x=y) + z       # if y==x from a previous call of
                                         # total(), used cached result.

myobj = MyObject(1, 2)

# Call total() with x=20
a = dt.now()
myobj.total(x=20)
b = dt.now()
c = (b - a).total_seconds()

# Call subtotal() with y=21
a2 = dt.now()
myobj.subtotal(y=21, z=1)
b2 = dt.now()
c2 = (b2 - a2).total_seconds()

# Call subtotal() with y=20 - should take substantially less time
# with x=20 used in previous call of total().
a3 = dt.now()
myobj.subtotal(y=20, z=1)
b3 = dt.now()
c3 = (b3 - a3).total_seconds()

print('c: {}, c2: {}, c3: {}'.format(c, c2, c3))
c: 2.469753, c2: 2.355764, c3: 0.016998

С Python3. 2 или новее, вы можете использовать functools.lru_cache. Если вы должны были украсить total с functools.lru_cache непосредственно, то lru_cache будет кэшировать возвращаемые значения total на основе значения обоих аргументов, self и x. Поскольку внутренний dict lru_cache хранит ссылку на self, применение @lru_cache непосредственно к методу класса создает циклическую ссылку на self, которая делает экземпляры класса неотразимыми (следовательно, утечка памяти).

Вот обходной путь , который позволяет использовать lru_cache с методами класса - он кэширует результаты, основанные на всех аргументах, кроме первого, self, и использует weakref , чтобы избежать проблемы циклической ссылки:

import functools
import weakref

def memoized_method(*lru_args, **lru_kwargs):
    """
    https://stackoverflow.com/a/33672499/190597 (orly)
    """
    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


class MyObject(object):

    def __init__(self, a, b):
        self.a, self.b = a, b

    @memoized_method()
    def total(self, x):
        print('Calling total (x={})'.format(x))
        return (self.a + self.b) * x


    def subtotal(self, y, z):
        return self.total(x=y) + z 

mobj = MyObject(1,2)
mobj.subtotal(10, 20)
mobj.subtotal(10, 30)

Отпечатки

Calling total (x=10)

Только один раз.


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

class MyObject(object):

    def __init__(self, a, b):
        self.a, self.b = a, b
        self._total = dict()

    def total(self, x):
        print('Calling total (x={})'.format(x))
        self._total[x] = t = (self.a + self.b) * x
        return t

    def subtotal(self, y, z):
        t = self._total[y] if y in self._total else self.total(y)
        return t + z 

mobj = MyObject(1,2)
mobj.subtotal(10, 20)
mobj.subtotal(10, 30)

Одно из преимуществ lru_cache над этим кэшем на основе dict состоит в том, что lru_cache является потокобезопасным. lru_cache также имеет параметр maxsize, который может помощь защита от роста использования памяти без привязки (например, из-за длительный процесс, вызывающий total много раз с различными значениями x).

В этом случае я бы сделал что-то простое, возможно, не самый элегантный способ, но работает для Проблемы:

class MyObject(object):
    param_values = {}
    def __init__(self, a, b):
        self.a, self.b = a, b

    def total(self, x):
        if x not in MyObject.param_values:
          MyObject.param_values[x] = (self.a + self.b) * x
          print(str(x) + " was never called before")
        return MyObject.param_values[x]

    def subtotal(self, y, z):
        if y in MyObject.param_values:
          return MyObject.param_values[y] + z
        else:
          return self.total(y) + z
    Ничего не найдено.

Добавить ответ:
Отменить.