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)
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(). 
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.
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.
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. 
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With