Как кешировать возвращаемое методом значение, которое зависит от атрибутов других классов? - PullRequest
5 голосов
/ 19 апреля 2011

Упрощенный код (без кеширования)

Сначала фрагмент упрощенного кода, который я буду использовать для объяснения проблемы.

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 является основным классом. Он имеет Engine и Chassis.

average_acceleration() использует атрибуты Engine и Chassis и выполняет очень трудоемкие вычисления.

velocity(), с другой стороны, выполняет очень простые вычисления, но использует значение, вычисленное как average_acceleration()

distance() передает velocity функцию на integrate()

Теперь integrate() звонит много раз velocity(), который звонит каждый раз average_acceleration(). Учитывая, что значение, возвращаемое average_acceleration(), зависит только от Engine и Chassis, было бы желательно кэшировать значение, возвращаемое average_acceleration().

Мои идеи

Первая попытка (не работает)

Fist I об использовании memoize decorator следующим образом:

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

Но это не будет работать так, как я хочу, потому что Engine и Chassis изменчивы. Таким образом, если сделать:

chassis.weight = new_value

average_acceleration () вернет неправильное (ранее кэшированное) значение при следующем вызове.

Вторая попытка

Наконец я изменил код следующим образом:

    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

Я добавил параметр acceleration в метод velocity(). Добавив эту опцию, я вычисляю acceleration только один раз в методе distance(), где я знаю, что объекты шасси и двигателя не изменены, и я передаю это значение скорости.

Итог

Код, который я написал, делает то, что мне нужно, но мне интересно, можете ли вы придумать что-нибудь лучше / чище?

Ответы [ 3 ]

2 голосов
/ 19 апреля 2011

Фундаментальная проблема - та, которую вы уже определили: вы пытаетесь memoize функция, которая принимает изменяемые аргументы. Эта проблема очень тесно связана с причиной, по которой python dict не принимает изменяемые встроенные модули в качестве ключей.

Это также проблема, которую очень просто исправить. Напишите функцию, которая принимает только неизменяемые аргументы memoize, а затем создайте функцию-оболочку, которая извлекает неизменяемые значения из изменяемых объектов. Итак ...

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)

Другим вариантом будет использование установщиков свойств для обновления значения average_acceleration при изменении соответствующих значений Engine и Chassis. Но я думаю, что это может быть более громоздким, чем выше. Обратите внимание, что для того, чтобы это работало, вы должны использовать классы нового стиля (то есть классы, которые наследуются от object - что вы действительно должны делать в любом случае).

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

Затем в Car.average_acceleration() проверьте, является ли engine.updated, пересчитайте aa, если это так, и установите для engine.updated значение False. Довольно неуклюжий, мне кажется.

0 голосов
/ 19 апреля 2011

Почему бы просто не назначить длинный расчет как свойство и вычислить его при инициализации?Если вам нужно вычислить это снова (например, вы меняете двигатель), тогда и только тогда вам нужно будет вызвать его снова.

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 голосов
/ 19 апреля 2011

В PyPI доступны различные реализации декоратора, связанные с кэшированием возвращаемого значения и учетом параметров функции.

Проверка gocept.cache или plone.memoize на PyPI.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...