MarkS
MarkS

Reputation: 1539

Class variable not updating

I have the following basic class involving a circle. My question is, when I directly update the diameter with c.diameter(10), how come the radius is updated to 5? As the output shows, it is still at 4:

from math import pi


class Circle:
    def __init__(self, radius=1):
        self.radius = radius
        self.diameter = self.radius * 2

    def diameter(self, diameter):
        self.diameter = diameter
        self.radius = self.diameter / 2
        return self.radius, self.diameter

    def area(self):
        return self.radius ** 2 * pi


c = Circle(3)
print("c.radius:", c.radius)
print("c.area():", c.area())
c.radius = 4  # change radius
print("c.radius:", c.radius)
print("c.area():", c.area())
c.diameter = 10  # change diameter
print("c.diameter:", c.diameter)
print("c.radius:", c.radius)

Output:

c.radius: 3
c.area(): 28.274333882308138
c.radius: 4
c.area(): 50.26548245743669
c.diameter: 10
c.radius: 4   <--- Radius should be 5, since the Radius is Diameter / 2

Upvotes: 0

Views: 10002

Answers (2)

broadcoder
broadcoder

Reputation: 105

One way is to use properties with according setters to make sure you update your internal data:

Something like this:

from math import pi


class Circle:
    def __init__(self, radius=1):
        self._radius = radius
        self._diameter = radius * 2

    @property
    def diameter(self):
        return self._diameter

    @diameter.setter
    def diameter(self, diameter):
        self._diameter = diameter
        self._radius = diameter / 2

    @property  
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, radius):
        self._radius = radius
        self._diameter = radius * 2

    def area(self):
        return self.radius ** 2 * pi

if __name__ == "__main__":
    c = Circle(3)
    print("c.radius:", c.radius)
    print("c.area():", c.area())
    c.radius = 4  # change radius
    print("c.radius:", c.radius)
    print("c.area():", c.area())
    c.diameter = 10  # change diameter
    print("c.diameter:", c.diameter)
    print("c.radius:", c.radius)

Upvotes: 0

Martijn Pieters
Martijn Pieters

Reputation: 1121924

You have two things named diameter:

  • An attribute on the instance, set with self.diameter in __init__ and with c.diameter = 10 later on.
  • A method on the class named diameter. This method is never accessed. You can't access it on the instance, because there the diameter attribute masks the method.

On Python classes, methods are still just attributes. c.area returns the method object, and only c.area() actually calls the method.

So, just referencing c.diameter will not actually give you the method, it gives you the attribute on the instance, which is just an integer object.

You have two options:

  • Rename the method, to set_diameter() for example.
  • Make the diameter attribute a property object. Properties are used as you would an attribute, but getting or setting the attribute on an instance triggers methods to be called.

The latter option is the 'pythonic' option, the method experienced Python developers would use:

class Circle:
    def __init__(self, radius=1):
        self.radius = radius

    @property
    def diameter(self):
        return self.radius * 2

    @diameter.setter
    def diameter(self, value):
        self.radius = value / 2

    def area(self):
        return self.radius ** 2 * pi

The @property / @diameter.setter pair of decorators define the getter and setter for the property; the first def diameter is called whenever you want to read (get) the value of the attribute, and the second is used when writing called when you try to assign a new value (the setter):

>>> c = Circle(3)
>>> c.diameter
6
>>> c.diameter = 4
>>> c.radius
2.0
>>> c.diameter
4.0

Note that we never set an attribute named diameter in the setter! The value is instead always calculated when you access c.diameter for reading.

You'll also notice that when you assign an integer to the diameter attribute, that the radius becomes a float value; that's because the / operator always produces a float value, even for integer inputs. Use // (floor division), if you always must have an integer.

Upvotes: 3

Related Questions