Raketenolli
Raketenolli

Reputation: 745

Is it OK/pythonic to separate a class' attribute and the method determining it?

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

Answers (4)

skyking
skyking

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

Andreas Grapentin
Andreas Grapentin

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

zmbq
zmbq

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 @propertys as well)

Upvotes: 1

Meloman
Meloman

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

Related Questions