如何缓存依赖于其他类属性的方法返回值?
简化代码(没有缓存)
首先,这里有一段简化的代码,我将用它来解释问题。
def integrate(self, function, range):
# this is just a naive integration function to show that
# function needs to be called many times
sum = 0
for x in range(range):
sum += function(x) * 1
return sum
class Engine:
def __init__(self, capacity):
self.capacity = capacity
class Chasis:
def __init__(self, weigth):
self.weight = weight
class Car:
def __init__(self, engine, chassis):
self.engine = engine
self.chassis = chassis
def average_acceleration(self):
# !!! this calculations are actually very time consuming
return self.engine.capacity / self.chassis.weight
def velocity(self, time):
# here calculations are very simple
return time * self.average_acceleration()
def distance(self, time):
2 + 2 # some calcs
integrate(velocity, 2000)
2 + 2 # some calcs
engine = Engine(1.6)
chassis = Chassis(500)
car = Car(engine, chassis)
car.distance(2000)
chassis.weight = 600
car.distance(2000)
问题
Car
是主要的类。它有一个 Engine
(引擎)和一个 Chassis
(底盘)。
average_acceleration()
这个方法使用了引擎和底盘的属性,并进行了一些非常耗时的计算。
而 velocity()
方法则进行一些非常简单的计算,但它需要用到 average_acceleration()
计算出来的值。
distance()
方法把 velocity
函数传递给 integrate()
。
现在,integrate()
多次调用 velocity()
,而每次调用 velocity()
时又会调用 average_acceleration()
。考虑到 average_acceleration()
返回的值只依赖于引擎和底盘,最好能缓存 average_acceleration()
返回的值。
我的想法
第一次尝试(不成功)
我首先考虑使用一个 记忆装饰器,像这样:
@memoize
def average_acceleration(self, engine=self.engine, chassis=self.chassis):
# !!! this calculations are actually very time consuming
return engine.capacity / chassis.weight
但这样做并不能达到我的目的,因为引擎和底盘是可变的。因此,如果我这样做:
chassis.weight = new_value
下次调用时,average_acceleration()
会返回错误的(之前缓存的)值。
第二次尝试
最后,我把代码修改成了这样:
def velocity(self, time, acceleration=None):
if acceleration is None:
acceleration = self.average_acceleration()
# here calculations are very simple
return time * acceleration
def distance(self, time):
acceleration = self.average_acceleration()
def velocity_withcache(time):
return self.velocity(time, acceleration)
2 + 2 # some calcs
integrate(velocity_withcache, 2000)
2 + 2 # some calcs
我在 velocity()
方法中添加了一个 acceleration
参数。这样一来,我只在 distance()
方法中计算一次 acceleration
,因为在这个方法里我知道底盘和引擎对象没有改变,然后把这个值传递给 velocity
。
总结
我写的代码能满足我的需求,但我很好奇你们是否能想出更好或更简洁的方案?
3 个回答
在PyPI上,有很多不同的装饰器实现可以用来缓存函数的返回值,并且还会考虑到函数的参数。
你可以去PyPI上看看 gocept.cache 或者 plone.memoize 这两个库。
为什么不直接把这个复杂的计算当作一个属性来处理,然后在初始化的时候计算一次呢?如果你之后需要再计算一次(比如你换了引擎),那时候再调用这个计算就可以了。
class Car:
def __init__(self, engine, chassis):
self.engine = engine
self.chassis = chassis
self.avg_accel = self.average_acceleration()
def average_acceleration(self):
# !!! this calculations are actually very time consuming
return self.engine.capacity / self.chassis.weight
def velocity(self, time):
# here calculations are very simple
return time * self.avg_accel
def distance(self, time):
2 + 2 # some calcs
integrate(velocity, 2000)
2 + 2 # some calcs
def change_engine(self, engine):
self.engine = engine
self.avg_accel = self.average_acceleration()
根本问题是你已经发现的:你在尝试对一个接受可变参数的函数进行memoize
。这个问题和为什么Python的dict
不允许可变内置类型作为键是密切相关的。
不过,这个问题其实很简单就能解决。你可以写一个只接受不可变参数的函数,对它进行memoize
,然后再创建一个包装函数,从可变对象中提取不可变的值。所以……
class Car(object):
[ ... ]
@memoize
def _calculate_aa(self, capacity, weight):
return capacity / weight
def average_acceleration(self):
return self._calculate_aa(self.engine.capacity, self.chassis.weight)
你还有一个选择,就是使用属性设置器,在相关的Engine
和Chassis
值发生变化时更新average_acceleration
的值。但我觉得这可能比前面的方法更麻烦。需要注意的是,要让这个方法有效,你必须使用新式类(也就是继承自object
的类——其实你应该一直这样做)。
class Engine(object):
def __init__(self):
self._weight = None
self.updated = False
@property
def weight(self):
return self._weight
@weight.setter
def weight(self, value):
self._weight = value
self.updated = True
然后在Car.average_acceleration()
中检查engine.updated
,如果是的话就重新计算加速度,并将engine.updated
设置为False。听起来有点繁琐,我觉得。