Properties let you add constraints to existing code without changing the interface. Let's say you started with a simple class:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
People use this class in many places, and in each place the attributes name
and age
are accessed directly.
At some later time, you realize that you shouldn't be able to make the age negative, and maybe you'd like to make sure the age is actually an int
. If you tried to add a new method like set_age
to restrict the values one could assign to age
, you would have to update all the existing code (which may not be something you can do, if Person
is part of a library that anyone can use).
Instead, you change age
from an ordinary instance attribute to a property.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, int) or int < 0:
raise ValueError("Age must be a non-negative integer")
self._age = age
Now, all the code like
p = Person("Alice", 13)
p.age = 14
continues to work exactly the way it did before. Code like
p = Person("Bob", 10)
p.age = -10
or
p = Person("Cassie", "old")
will now raise ValueError
s as desired.
Properties also let you define "computed attributes", which are not necessarily stored, but re-computed from other attribute values as necessary. A simple example:
import math
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return math.pi * self.radius ** 2
@property
def perimeter(self):
return 2 * self.radius * math.pi
Neither the area nor the perimeter is stored, but will always return the correct value, no matter how often the radius is changed. Note, too, that you cannot assign directly to the area or the perimeter, because no setter was defined. You can only change either by changing the radius.
If the opertation is particularly expensive, you can cache the result to be re-used.
class Circle:
def __init__(self, radius):
self.radius = radius
self._area = None # Cached radius
# We could still access radius directly, but
# we want to "catch" changes to the radius
# to invalidate the stored area.
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, radius):
self._radius = radius
self._area = None # Invalidate the cache
# Too expensive, let's re-use a previous calculation
# if the radius hasn't changed.
@property
def area(self):
if self._area is None:
self._area = math.pi * self.radius ** 2
return self._area
# Cheap enough to not bother caching.
@property
def perimeter(self):
return 2 * self.radius * math.pi