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 Line
s 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
2 Answers 2
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
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)
-
\$\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\$stenci– stenci2015年03月20日 21:40:50 +00:00Commented 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\$Echocage– Echocage2015年03月20日 21:49:56 +00:00Commented Mar 20, 2015 at 21:49
Explore related questions
See similar questions with these tags.
start
andend
are mutable, based on statements likeself.start.rotate(pivot_point, factor)
andself.end.scale(pivot_point, factor)
. \$\endgroup\$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\$