开发者

How to cache value returned by method, which is dependent on attributes of other classes?

Simplified code (no caching)

First a piece of simplified code, which I'll use to explain the problem.

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)

Problem

Car is the main class. It has an Engine and a Chassis.

average_acceleration() uses attributes from Engine and Chassis and performs very time consuming calculations.

velocity(), on the other hand, perfoms very simple calculations, but uses a value calculated by average_acceleration()

distance() passes velocity function to integrate()

Now, integrate() calls many times velocity(), which calls each time average_acceleration(). Considering that the value returned by average_acceleration() depends only on Engine and Chassis, it'd be desirable to cache the value returned by average_acceleration().

My ideas

First attempt (not working)

Fist I though about using a memoize decorator in the following manner:

    @memoize
    def average_acceleration(self, engine=self.engine, chassis=self.chassis):
        # !!! this calculations are actually very time consuming
        return engine.capacity / chassis.weight

But it won't work as I want, because Engine and Chassis are mutable. Thus, if do:

chassis.weight = new_value

average_acceleration() will return wrong (previously cached) value on the next call.

Second attempt

Finally I modified the code as follows:

    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

I added the parameter acceleration to velocity() method. Having that option added, I calculate acceleration only once in distance() method, where I know that chassis and engine objects are not changed and I pass this value to velocity.

Bottom line

The code I wrote does what I need it to do, but I'm curious if you can come up with someting better/cleaner?


The fundamental problem is one that you've already identified: you're trying to memoize a function that accepts mutable arguments. This problem is very closely related to the reason python dicts don't accept mutable built-ins as keys.

It's also a problem that's very simple to fix. Write a function that only accepts immutable arguments, memoize that, and then create a wrapper function that extracts the immutable values from the mutable objects. So...

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)

Your other option would be to use property setters to update the value of average_acceleration whenever relevant values of Engine and Chassis are changed. But I think that might actually be more cumbersome than the above. Note that for this to work, you have to use new-style classes (i.e. classes that inherit from object -- which you should really be doing anyway).

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

Then in Car.average_acceleration() check whether engine.updated, recalculate aa if so, and set engine.updated to False. Pretty clunky, seems to me.


There are various decorator implementations available on PyPI dealing with caching return value and taking the function parameters into account.

Check for gocept.cache or plone.memoize on PyPI.


Why not just assign the long calculation as a property, and calculate it on initialization? If you need to calculate it again (e.g. you change the engine) then and only then would you need to call it again.

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

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜