Как кэшировать значение, возвращаемое методом, которое зависит от атрибутов других классов?

#python #oop

#python #ооп

Вопрос:

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

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

 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 и a Chassis .

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

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

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

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

Мои идеи

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

Сначала я подумал об использовании декоратора memoize следующим образом:

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

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

 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() методе, где я знаю, что объекты шасси и движка не изменены, и я передаю это значение в velocity.

Итог

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

Комментарии:

1. 1, Вероятно, лучший письменный вопрос домашнего задания, который я видел. (Я предполагаю, что это домашнее задание)

2. Спасибо. На самом деле это не домашнее задание, но я не профессиональный программист, поэтому, думаю, это может выглядеть как таковое 😉

3. Если ускорение постоянное (как это представляется в distance методе), то вы должны быть в состоянии вычислить расстояние с помощью исчисления, вычислительная интеграция не требуется. distance = 1/2 * acceleration * time**2 .

4. @unutbu, очевидно, что это, как я уже указывал, упрощенный код. Мои реальные вычисления намного сложнее, я просто попытался изложить здесь, в чем заключается моя проблема с кэшированием.

Ответ №1:

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

Это также проблема, которую очень просто исправить. Напишите функцию, которая принимает только неизменяемые аргументы, 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 . Мне кажется, довольно неуклюжий.

Комментарии:

1. По сути, это исправление для моего первого подхода. Я уже рассматривал это решение, и проблема, которую я вижу, заключается в том, что, что если self.engine.capacity и self.chassis.weight также изменяемы? Тогда мне пришлось бы пойти «на один уровень глубже», верно? Это потенциально громоздко. Но, возможно, это единственное решение, если я хочу использовать запоминание, потому что должен быть создан ключ кэша, содержащий все переменные. С другой стороны, может быть, есть какое-то другое разумное решение.

2. Честно говоря, другое решение этой проблемы, которое я вижу, более громоздкое; смотрите выше.

3. Кроме того, разве вам уже не нужно идти «на один уровень глубже», чтобы выполнить свои вычисления? Конечно, ваши вычисления выполняются на неизменяемых объектах. Так что просто memoize на любом самом низком уровне.

4. Это то, что @Zirak предложил в своем ответе, и я согласен, что это менее хорошо. Тем не менее, из этих двух вариантов и моего решения я выбираю свое. Но я думаю, что ваше решение было бы единственным решением в случае более произвольного доступа к average_acceleration() .

5. Честно говоря, чем больше я думаю об этом, тем больше я чувствую, что мое решение является лучшим, потому что оно обеспечивает большее разделение проблем. У вас не должно быть функции, которая выполняет сложные вычисления и одновременно перемещается по сложной иерархии объектов. Перейдите по иерархии объектов в отдельной функции и передайте неизменяемые объекты другой функции для вычисления.

Ответ №2:

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

Проверьте наличие gocept.cache или plone.memoize в PyPI.

Ответ №3:

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

 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()
  

Комментарии:

1. Что в случае, если движок был бы изменен не через интерфейс Car (вне класса)? Движок должен быть подвергнут глубокой обработке, чтобы заставить его работать. Но даже тогда нельзя было внести никаких изменений в engine внутри класса car, можно было создать только новый экземпляр (или глубокую копию). Кроме того, потребуется настройка для каждого компонента, т. е. движка, шасси и т.д. Это кажется более запутанным, чем мое решение.

2. Почему? У вас все та же ссылка на движок, и, как уже было сказано, если вы хотите внести какие-либо изменения в движок, повторно запустите операцию.

3. Я имею в виду, если я сделаю что-то вроде этого: engine.capacity = 800 , кэшированное значение average_acceleration не будет сброшено.

4. Чем это отличается от вашего решения?