Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

python

oop

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?

like image 310
Patrick Avatar asked Nov 05 '22 23:11

Patrick


1 Answers

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.

like image 57
senderle Avatar answered Nov 11 '22 04:11

senderle