Python:根据新的函数参数调用缓存的函数结果



我对缓存和记忆的概念相当陌生。 我已经在这里,这里和这里阅读了其他一些讨论和资源,但无法很好地遵循它们。

假设我在一个类中有两个成员函数。 (下面是简化的示例。 假设第一个函数total计算成本很高。 第二个函数subtotal计算简单,除了它使用第一个函数的返回,因此计算成本也因此变得昂贵,因为它当前需要重新调用total才能获得返回的结果。

我想缓存第一个函数的结果并将其用作第二个函数的输入,如果输入ysubtotal共享最近调用total的输入x。 那是:

  • 如果调用 subtotal() 其中y等于先前调用total中的
    x值,则使用该缓存结果而不是
    重新调用total
  • 否则,只需使用x = y调用total()

例:

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

使用 Python3.2 或更高版本,您可以使用functools.lru_cache。 如果要直接用functools.lru_cache装饰total,则lru_cache将根据两个参数的值(selfx)缓存total的返回值。由于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的一个优点是lru_cache是线程安全的。lru_cache还有一个maxsize参数,可以帮助 防止内存使用量无限制地增长(例如,由于 长时间运行的进程调用total多次,具有不同的x值)。

谢谢大家的回复,阅读它们并了解引擎盖下发生的事情很有帮助。 正如@Tadhg麦当劳詹森所说,我似乎在这里不需要比@functools.lru_cache更多的东西。 (我在 Python 3.5 中。 关于@unutbu的评论,我没有收到用@lru_cache装饰 total() 的错误。 让我纠正我自己的例子,我会在这里为其他初学者保留这个:

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

在这种情况下,我会做一些简单的事情,也许不是最优雅的方式,但可以解决问题:

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

最新更新