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 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.
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()
精彩评论