2
\$\begingroup\$

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
200_success
145k22 gold badges190 silver badges478 bronze badges
asked Mar 20, 2015 at 20:26
\$\endgroup\$
5
  • \$\begingroup\$ @200_success: why was the title of my post changed, and with it also the meaning, which created confusion? My question was about "Lazy properties in Python when the values can be changed by a method", not about "Lazy properties for the angle and length of a line segment" \$\endgroup\$ Commented Mar 20, 2015 at 21:34
  • \$\begingroup\$ Site standards require the title to reflect the purpose of your code — see How to Ask. Despite the title change, it should still be apparent that start and end are mutable, based on statements like self.start.rotate(pivot_point, factor) and self.end.scale(pivot_point, factor). \$\endgroup\$ Commented Mar 20, 2015 at 21:46
  • \$\begingroup\$ I did read the how to ask. If you search for [design-patterns] you can see 621 questions similar to mine. I was asking about a design-pattern, as described by the title and by the tag. My question has nothing to do with lines and angles, has to do with properties. I think that the editing was just wrong and misleading, infact it attracted the wrong answer \$\endgroup\$ Commented Mar 20, 2015 at 21:54
  • 2
    \$\begingroup\$ If you are asking about generic best practices (where your Line class is nothing but a hypothetical illustration) rather than seeking a code review, then Software Engineering would be a more appropriate place to ask. \$\endgroup\$ Commented Mar 20, 2015 at 21:55
  • 2
    \$\begingroup\$ Thanks for the suggestion. It would have been nicer this suggestion than assigning the wrong title and the wrong tags. \$\endgroup\$ Commented Mar 20, 2015 at 21:58

2 Answers 2

2
\$\begingroup\$

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
answered Mar 20, 2015 at 21:54
\$\endgroup\$
1
\$\begingroup\$

Just changed the properties so it's more obvious what you're doing in them, but other than that this is a fine way of doing it!

import math
class Line:
 @property
 def angle(self):
 if not hasattr(self, '_angle'):
 self._angle = math.atan2(self.end.y - self.start.y, self.end.x - self.start.x)
 return self._angle
 @property
 def length(self):
 if not hasattr(self, '_length'):
 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_attribute(self, attribute):
 if hasattr(self, attribute):
 delattr(self, attribute)
 def _reset_attributes(self, attributes):
 for attribute in attributes:
 self._reset_attribute(attribute)
answered Mar 20, 2015 at 21:26
\$\endgroup\$
2
  • \$\begingroup\$ You also changed from EAFP to LBYL. I thought EAFP was the pythonic way. Any comment on that? (see docs.python.org/3.3/glossary.html#term-eafp) \$\endgroup\$ Commented Mar 20, 2015 at 21:40
  • \$\begingroup\$ While in many cases I find EAFP useful, in this case I think LBYL improved readability. In my experience, EAFP is used when you're catching a case in which you've made a false assumption, like a key should be there, but it's not, so you catch the error it throws. In this case, it shouldn't be there initially, we know it won't be the first time each property is accessed, in my mind it makes more sense to explicitly check in that case. In this case I'm trading the microseconds it takes to make that check, for more readable and understandable code. \$\endgroup\$ Commented Mar 20, 2015 at 21:49

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.