Reputation: 745
I hope this question hasn't been asked before, my google/SX-fu is not very good on this one because I might not know the proper keywords.
Assume I have a class that represents a rather complex object, e. g. a point cloud, that has certain properties (a length, a volume...). Typically, I would go about defining a class for the point cloud (or in this case, a rectangle), like this (example courtesy of A Beginner's Python Tutorial, modified):
class Rectangle:
def __init__(self,x,y):
self.x = x
self.y = y
def area(self):
return self.x * self.y
def perimeter(self):
return 2 * self.x + 2 * self.y
And whenever I need to know the area of the rectangle, I just call my_rectangle.area()
, which will always give me the proper result, even if the dimensions of the rectangle change.
Now in my application, calculating the perimeter or the area is a lot more complex and takes quite a bit of time. Also, typically, I need to know the perimeter more often than I modify the object. So it would make sense to split the calculation from accessing the value itself:
class Rectangle:
def __init__(self,x,y):
self.x = x
self.y = y
def calc_area(self):
self.area = self.x * self.y
def calc_perimeter(self):
self.perimeter = 2 * self.x + 2 * self.y
Now, if I need to know the area of the rectangle, I need to call my_rectangle.calc_area()
at least once after any modification, but afterwards I can always just get my_rectangle.area
.
Is this a good idea or should I rather keep the area calculation in the .area()
method and access it whenever I need it, while storing the current area in a local variable in whichever script is using my Rectangle
class?
If this is too opinion-based or too dependent on the actual application, please advise on how to improve the question.
Upvotes: 2
Views: 171
Reputation: 14395
Yes, at least the principle. If it's worth caching the calculation on the other hand is a different question.
There are at least cases where attributes or properties are calculated the first time they are being used in the standard library - and what's in the standard library should probably be considered pythonic. The step from there to use it for property that can change during the lifetime of the object should not be considered too far (but you then need to remember to invalidate the cached value whenever it need to be recalculated).
However at such instances they use the @property
decorator to make the property to be accessed like it were an attribute. Also if dependent on other attributes we have an even more reason to use properties:
class Rectangle(object):
def __init__(self, w, h):
self._width = w
self._height = h
@property
def width(self):
return self._width
@width.setter
def width(self, w):
self._width = w
del self._area
@property
def height(self):
return self._height
@height.setter
def height(self, w):
self._height = w
del self._area
@cached
def area(self):
return self._width * self._height
Note the del self._area
in the setter for self.width
which will make next access to .area
to require recalculation of self._area
.
In this particular case you could have set self._area
to None
and checked for that as in the other answers. However this technique might not work if the attribute could take None
as a valid value. The try
-except
method can handle this possibility. For more general approach you could define your own decorator:
def cached(calc):
def getter(self):
key = "_" + calc.__name__
try:
return getattr(self, key)
except AttributeError:
val = calc(self)
setattr(self, key, val)
return val
return property(getter)
And then in the definition of Rectangle
instead have defined area
as:
@cached
def area(self):
return self._width * self._height
Upvotes: 2
Reputation: 5796
properties are indeed the way to go here. I would suggest something along the lines of the following:
class Rectangle:
def __init__(self, x, y):
# member names starting with underscore indicate private access
self._x = x
self._y = y
# it's good practice to initialize all members in __init__
self._area = None
@property
def area(self):
# this is a read-only property.
# access it like:
# rect = Rectangle(1, 1)
# print(rect.area)
# note the missing parentheses.
if self._area is None:
# lengthy computation here, but only when needed
self._area = self._x * self._y
return self._area
@property
def x(self):
# getter for self._x
return self._x
@x.setter
def x(self, value):
# setter for self._x
self._x = value
# invalidate self._area - it will get recalculated on next access
self._area = None
# getters and setters for y are similar.
Upvotes: 3
Reputation: 39023
First off, this seems like a good use for properties:
...
@property
def area(self):
...
Then you can access my_rectangle.area
.
If calculating the area is a lengthy process and you don't want your class users to bother with it, calculate it the first time the area
property is accessed. In subsequent accesses just return the calculated value.
If something in the object changes and the area needs to be recalculated, just mark the area as not calculated when x or y change (you can make them @property
s as well)
Upvotes: 1
Reputation: 104
Well, that's not really a "pythonic/not pythonic" question. You're asking about design: to cache or not to cache.
What I'd do is to calculate area and perimeter in the __init__
method. And if your class will be mutable (you will modify x and y from outside), you'll want to use setters, in which should also update the area and perimeter.
Also, keep in mind that this is a classical CPU/RAM tradeoff, so use it only if you get area and perimeter frequently enough, so it would make a difference in speed.
Upvotes: 0