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().
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
Comments