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 dict
s 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