Вычислить атрибут, если он не существует

Я пытаюсь получить доступ к атрибуту, который не должен быть создан в методе __init__ моего класса, но может быть рассчитан путем вызова другого метода. Я пытаюсь сделать так, чтобы, если я попытаюсь получить доступ к атрибуту, и он не существует, он будет автоматически рассчитан. Тем не менее, я не хочу, чтобы он был пересчитан, если атрибут существует, даже если значение будет отличаться. Например:

class SampleObject(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def calculate_total(self):
        self.total = self.a + self.b

sample = SampleObject(1, 2)
print sample.total   # should print 3
sample.a = 2
print sample.total   # should print 3
sample.calculate_total()
print sample.total   # should print 4

Мое лучшее решение до сих пор заключается в создании метода get_total(), который делает то, что мне нужно.

class SampleObject2(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def calculate_total(self):
        self.total = self.a + self.b

    def get_total(self):
        if hasattr(self, 'total'):
            return self.total
        else:
            self.calculate_total()
            return self.total

sample2 = SampleObject2(1, 2)
print sample2.get_total() # prints 3
sample2.a = 2
print sample2.get_total() # prints 3
sample2.calculate_total()
print sample2.get_total() # prints 4

Это прекрасно работает, но я прочитал, что использование getters в python не рекомендуется, и я надеялся избежать вызова этой функции каждый раз, когда захочу получить доступ к этому атрибуту. Это мое лучшее решение, или есть более чистый, более питонический способ сделать это?

Это пример, который я составил. В моей фактической проблеме calculate_total() - это процесс, требующий много времени, который необязательно должен быть вызван. Поэтому я не хочу выполнять его в методе init.

Ответ 1

Вы хотите использовать декоратор @property. Создайте метод, к которому будет обращаться как обычный атрибут, который делает ленивое вычисление:

class SampleObject:

    def __init__(self):
        # ...
        self._total = None

    @property
    def total(self):
        """Compute or return the _total attribute."""
        if self._total is None:
            self.compute_total()

        return self._total

Ответ 2

Пирамида (веб-фреймворк) поставляется с декоратором reify, который похож на property (показан Остином Гастингсом), но он работает несколько иначе: функция выполняется только один раз, а после этого возвращаемое значение по функции всегда используется. Это, по сути, делает то, что делает код Austin, но без использования отдельного атрибута: это обобщение этого шаблона.

Вероятно, вы не хотите использовать всю веб-инфраструктуру только для этого одного декоратора, так что вот эквивалент, который я написал:

import functools

class Descriptor(object):
    def __init__(self, func):
        self.func = func
    def __get__(self, inst, type=None):
        val = self.func(inst)
        setattr(inst, self.func.__name__, val)
        return val

def reify(func):
    return functools.wraps(func)(Descriptor(func))

Использование:

class ReifyDemo:
    @reify
    def total(self):
        """Compute or return the total attribute."""
        print("calculated total")
        return 2 + 2    # some complicated calculation here

r = ReifyDemo()
print(r.total)     # prints 'calculated total 4' because the function was called
print(r.total)     # prints just '4` because the function did not need to be called