Properties

Getters

Let’s create a shapes.py file and in it create a Circle class which includes attributes of a radius and an area:

import math

class Circle:
    def __init__(self, radius=1):
        self.radius = radius
        self.area = math.pi * self.radius ** 2

This works, but when we change our radius, the area doesn’t change:

>>> from shapes import Circle
>>> circle = Circle()
>>> circle.radius
1
>>> circle.area
3.141592653589793
>>> circle.radius = 5
>>> circle.area
3.141592653589793

We could make a “getter” method that returns the area:

import math

class Circle:

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

    def get_area(self):
        return math.pi * self.radius ** 2

But that would result in a somewhat awkward interface. We would have to type circle.get_area() instead of circle.area.

Instead, let’s make this method into a property by using the property decorator. The decorator syntax, using the @ symbol, is a way to apply a “decorator function” to our own function.

Decorators are said to “wrap” a function because they can change the behavior of that function, usually adding some extra functionality before or after a function runs and sometimes entirely replacing the function.

Note

A decorator function is a function that takes a function as input and returns a new replacement function. For more on decorators, see the “Stuff we Skipped” section.

The property decorator, when applied to a method, makes the method name behave just like an attribute variable.

import math

class Circle:

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

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

This allows us to access our area attribute as if it was just any other attribute, not a method:

>>> from shapes import Circle
>>> circle = Circle()
>>> circle.radius
1
>>> circle.area
3.141592653589793
>>> circle.radius = 5
>>> circle.area
78.53981633974483

Setters

Let’s add a diameter property to our Circle class:

import math

class Circle:

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

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

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

Let’s try it out:

>>> from shapes import Circle
>>> circle = Circle()
>>> circle.radius
1
>>> circle.diameter
2
>>> circle.radius = 5
>>> circle.diameter
10

What if we want to set the diameter though?

>>> circle.diameter = 20
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

We can’t change the diameter because this is a read-only property.

To allow writing to this property, we have to make a setter method for it:

import math

class Circle:

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

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

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

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

The syntax is a little weird for that, but it works. In that @diameter.setter line we’re telling our already created diameter property object that we’d like to use a particular function as our setter method.

Now we can both get and set our diameter. When we change either the diameter or the radius, the area will always change appropriately:

>>> from shapes import Circle
>>> circle = Circle()
>>> circle.radius
1
>>> circle.diameter
2
>>> circle.diameter = 10
>>> circle.radius
5.0
>>> circle.area
78.53981633974483
>>> circle.radius = 2
>>> circle.diameter
4
>>> circle.area
12.566370614359172

This feature of Python allows more control of the attributes of classes. For example, perhaps our Circle class is defined in a specific application where it does not make sense for the radius to be less than 1.0. We could use a property.setter decorator to check the new radius value, and raise a ValueError if it is an invalid number.

In this way, we can always make sure that our attributes contain valid data.

Property Exercises

Point with Magnitude

This is the Point exercise in properties.py.

Make a Point class that has an auto-calculated magnitude attribute. The magnitude should be calculated by sqrt(x**2 + y**2 + z**2).

Note

To test these changes, you should modify the PointTests class in properties_test.py to comment out the @unittest.skip for “magnitude property”.

Example:

>>> from properties import Point
>>> p = Point(2, 3, 6)
>>> p.magnitude
7.0
>>> p.y = 9
>>> p.magnitude
11.0

Full Name

This is the Person exercise in properties.py.

Create the Person class so that it has a first_name and last_name attribute and a name property which is the combination first_name and last_name, with a space between.

>>> from properties import Person
>>> trey = Person("Trey", "Hunner")
>>> trey.name
'Trey Hunner'
>>> trey.last_name = "Smith"
>>> trey.name
'Trey Smith'

Circle

This is the Circle exercise in properties.py.

Create the Circle class so that it:

  • can be constructed with an explicit radius or the default radius of 1

  • has an area property which auto-updates based on radius changes

  • has a diameter property which auto-updates based on radius changes

  • auto-updates the radius property based on diameter changes

Example usage:

>>> from properties import Circle
>>> circle1 = Circle()
>>> circle.radius
1
>>> circle.area
3.141592653589793
>>> circle.diameter
>>> circle.radius = 2
>>> circle.area
12.566370614359172
>>> circle.diameter
4
>>> circle.diameter = 10
>>> circle.radius
5.0
>>> circle.area
78.53981633974483

Log Radius Changes

This is the Circle exercise in properties.py.

Modify the Circle class we defined previously to use a property for the radius and record changes to the radius in a radius_changes attribute.

Note

To test these changes, you should modify the CircleTests class in properties_test.py to comment out the @unittest.skip for the lines from the appropriate test methods.

Hint

You will need to store the actual radius somewhere. You may want to use a _radius attribute when converting the radius to a property.

Example usage:

>>> from properties import Circle
>>> circle = Circle()
>>> circle.radius
1
>>> circle.radius_changes
[1]
>>> circle.radius = 2
>>> circle.radius = 3
>>> circle.radius_changes
[1, 2, 3]

Set Radius Error

This is the Circle exercise in properties.py.

Edit the Circle class and modify it again to raise a ValueError when the radius or diameter are set to a negative number.

Note

To test these changes, you should modify the CircleTests class in properties_test.py to comment out the @unittest.skip for the lines from the appropriate test methods.

Example usage:

>>> from properties import Circle
>>> circle = Circle()
>>> circle.radius = -10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "shapes.py", line 18, in radius
    raise ValueError("Radius cannot be negative")
ValueError: Radius cannot be negative