如何缓存依赖于其他类属性的方法返回值?

4 投票
3 回答
572 浏览
提问于 2025-04-16 16:02

简化代码(没有缓存)

首先,这里有一段简化的代码,我将用它来解释问题。

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 个回答

0

在PyPI上,有很多不同的装饰器实现可以用来缓存函数的返回值,并且还会考虑到函数的参数。

你可以去PyPI上看看 gocept.cache 或者 plone.memoize 这两个库。

0

为什么不直接把这个复杂的计算当作一个属性来处理,然后在初始化的时候计算一次呢?如果你之后需要再计算一次(比如你换了引擎),那时候再调用这个计算就可以了。

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

根本问题是你已经发现的:你在尝试对一个接受可变参数的函数进行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)

你还有一个选择,就是使用属性设置器,在相关的EngineChassis值发生变化时更新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。听起来有点繁琐,我觉得。

撰写回答