Lazy properties for the angle and length of a line segment The code below shows the pattern I use to manage properties in my classes when I have the following requirements:


Some properties are slow to calculate and are rarely used, so I want them to be lazy
Some methods change the value of a property


The properties are defined the first time they are requested and are deleted when the methods are executed. Calculating the length in the Line class shown in the example is fairly fast, but calculating the Length of a shape made of many Lines would be slow and must be avoided unless required. The full code include many more properties and the list in _reset_attributes([...]) can have dozens of items.

Is this the correct/pythonic/fast way of managing properties?

class Line:
    @property
    def angle(self):
        try:
            return self._angle
        except AttributeError:
            self._angle = math.atan2(self.end.y - self.start.y, self.end.x - self.start.x)
            return self._angle

    @property
    def length(self):
        try:
            return self._length
        except AttributeError:
            self._length = math.sqrt((self.end.x - self.start.x) ** 2 + (self.end.y - self.start.y) ** 2)
            return self._length

    def rotate(self, pivot_point, angle):
        self.start.rotate(pivot_point, angle)
        self.end.rotate(pivot_point, angle)
        self._reset_attributes(['_angle'])

    def scale(self, pivot_point, factor):
        self.start.scale(pivot_point, factor)
        self.end.scale(pivot_point, factor)
        self._reset_attributes(['_length'])

    def _reset_attributes(self, attributes):
        for attribute in attributes:
            try:
                delattr(self, attribute)
            except:
                pass
You can create a custom decorator to cache the values, with the help of this decorator we can also remove the unnecessary repetitive if-else or try-except checks present in each of the properties. Hence, the methods will only contain the required code.

The trick here is to use each method's name and store an additional attribute on the instance itself with _ prefixed to it. So, when we look for a value we are first going to check if it is present on that instance, if yes then return the value otherwise call the function and store the value on the instance and return it.

def cached_property(func):
    attr = '_' + func.__name__

    class Descriptor(object):
        def __get__(self, ins, type):
            if hasattr(ins, attr):
                return getattr(ins, attr)
            else:
                value = func(ins)
                setattr(ins, attr, value)
                return value
    return Descriptor()


class Test(object):

    def __init__(self, name):
        self.name = name

    @cached_property
    def find_max(self):
        print ('property max of {!s} accessed'.format(self))
        lst = range(10**5)
        return max(lst)

    @cached_property
    def find_min(self):
        print ('property min of {!s} accessed'.format(self))
        lst = range(10**5, -1, -1)
        return min(lst)

    def __str__(self):
        return self.name


if __name__ == '__main__':
    t1 = Test('t1')
    t2 = Test('t2')
    print(t1.find_max)
    print(t1.find_min)
    print(t2.find_max)
    print(t2.find_min)
    print(t1.find_max,
    t1.find_min,
    t2.find_max,
    t2.find_min)

    t1.remove_value('_find_max')
    t2.remove_value('_find_min')

    print(t1.find_max)
    print(t1.find_min)
    print(t2.find_max)
    print(t2.find_min)


Output:

property max of t1 accessed
99999
property min of t1 accessed
0
property max of t2 accessed
99999
property min of t2 accessed
0
99999 0 99999 0
property max of t1 accessed
99999
0
99999
property min of t2 accessed
0


For removing a single value I would recommend defining __delete__ method on the descriptor itself:

def cached_property(func):
    attr = '_' + func.__name__

    class Descriptor(object):
        def __get__(self, ins, type):
            if hasattr(ins, attr):
                return getattr(ins, attr)
            else:
                value = func(ins)
                setattr(ins, attr, value)
                return value

        def __delete__(self, ins):
            try:
                delattr(ins, attr)
            except AttributeError:
                pass

    return Descriptor()


Now these two calls:

t1.remove_value('_find_max')
t2.remove_value('_find_min')


Can be replaced with:

del t1.find_max
del t2.find_min