Python:根据新的函数参数调用缓存的函数结果
Posted
技术标签:
【中文标题】Python:根据新的函数参数调用缓存的函数结果【英文标题】:Python: recall cached function result dependent on new function parameter 【发布时间】:2017-10-20 00:09:29 【问题描述】:我对缓存和记忆的概念相当陌生。我已经阅读了一些其他讨论和资源 here、here 和 here,但一直无法很好地关注它们。
假设我在一个类中有两个成员函数。 (下面的简化示例。)假设第一个函数total
计算量很大。第二个函数subtotal
在计算上很简单,除了它使用第一个函数的返回值,因此也变得计算量很大,因为它当前需要重新调用total
来获得它的返回结果。
我想缓存第一个函数的结果并将其用作第二个函数的输入,如果输入 y
到 subtotal
将输入 x
共享给最近的通话的total
。那就是:
y
等于 a 中 x
的值
total
的先前调用,然后使用该缓存结果而不是
重新调用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().
【问题讨论】:
你试过这个吗:***.com/a/18723434/2570677。我已经在我的代码中使用了它,它运行良好。 假设你指的是@functools.lru_cache
?
从您链接到的资源中,是什么阻止您仅使用基本缓存功能装饰total
?您只需输入@functools.lru_cache(maxsize=N)
,它就会缓存N
相同参数的结果。为什么这在您的场景中不起作用?
@BradSolomon 我指的是包含实现的答案(没有任何外部模块)。它适用于 python 2.7。
【参考方案1】:
对于 Python3.2 或更高版本,您可以使用functools.lru_cache
。
如果您要直接用functools.lru_cache
装饰total
,那么lru_cache
将根据self
和x
这两个参数的值缓存total
的返回值。由于 lru_cache 的内部 dict 存储对 self 的引用,因此将 @lru_cache 直接应用于类方法会创建对 self
的循环引用,这使得类的实例不可解除引用(因此内存泄漏)。
Here is a workaround 允许您将lru_cache
与类方法一起使用——它基于除第一个参数self
之外的所有参数缓存结果,并使用weakref 来避免循环引用问题:
import functools
import weakref
def memoized_method(*lru_args, **lru_kwargs):
"""
https://***.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)
只有一次。
或者,您可以通过以下方式使用 dict 滚动自己的缓存:
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
值)。
【讨论】:
【参考方案2】:感谢大家的回复,阅读它们并了解幕后情况很有帮助。正如@Tadhg McDonald-Jensen 所说,似乎我在这里不需要更多的东西,而不是@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
【讨论】:
self.a
和 self.b
会改变吗?如果是这样,缓存的值应该被清除,因为total
的计算值会改变。您可以通过设置a
和b
的setter 调用total.cache_clear()
的可设置属性来实现它。
PS:我将 lru_cache 应用于引发错误的类方法是错误的。虽然没有报错,不过it does cause a memory leak。
这里self.a
和self.b
不会被修改。不过谢谢,知道这很有帮助。【参考方案3】:
在这种情况下,我会做一些简单的事情,也许不是最优雅的方式,但可以解决问题:
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
【讨论】:
以上是关于Python:根据新的函数参数调用缓存的函数结果的主要内容,如果未能解决你的问题,请参考以下文章
设计一个函数,它接受不定数量的参数,这是参数都是函数。这些函数都接受一个回调函数作为参数,按照回调函数被调用的顺序返回函数名