A property is a built in method that returns or computers information associated with a given class. The information returned will relate to the class and instance attributes which were discussed here. Perhaps it will be useful to begin with how not to use properties. Take the following Movie class as an example; it takes a director, cost of production and revenue gained from the project.
class Movie:
def __init__(self, director, cost, revenue):
self.director = director
self.cost = cost
self.revenue = revenue
The property decorator denoted by @property is the most common way to declare a decorator in Python. The protocol for implementing a property that has read and write access is to include both a getter and a setter.
Although when naming properties we should be careful as you will see below:
Wrong way to name properties
class Movie:
def __init__(self, director, cost, revenue):
self.director = director
self.cost = cost
self.revenue = revenue
@property
def director(self):
return self.director
@director.setter
def director(self, new_director):
self.director = new_director
If we try to create an instance of the class above, it will result in infinite recursion, essentially this means that the code will not execute and the program will crash! So don't try this unless you are aware it will crash the kernel.
m = Movie('Mr. A', 100, 160)
Why does this happen?
This happens because when Python tries to set the variables, it returns the property as a callable as opposed to the desired behavior of returning a string of the director's name. Take the following analogous example that may be a simpler representation of what is happening here:
def f(x):
return f(x)
The function above clearly is going to call itself continually (until we get a maximum recursion error). This is similar to what is happening when we attempt to create an instance of our Movie class in its current form
Therefore it is advised to take care when naming properties and instance attributes. We can solve this in a number of ways.
Getter and Setter
Method 1- Make the instance attributes semi-private
All we need to do to fix this is to change the attributes within the constructor to be prefixed with an underscore.
class Movie:
def __init__(self, director, cost, revenue):
self._director = director
self.cost = cost
self.revenue = revenue
@property
def director(self):
print("Getter called for director")
return self._director
@director.setter
def director(self, new_director):
print("Setter called for director")
self._director = new_director
We can now create an instance and get the desired behavior from our class:
m1 = Movie('Mr. A', 100, 160)
m1.director
# returns 'getter called'
m1.director = "Mr. B"
# returns 'setter called'
m1.director
# returns 'getter called'
Method 2- Double underscore before the variable within the property
We can add __ within the property decorator. Implementing this method for the cost attribute below:
class Movie:
def __init__(self, director, cost, revenue):
self._director = director
self.cost = cost
self._revenue = revenue
@property
def director(self):
print("Getter called for director")
return self._director
@director.setter
def director(self, new_director):
print("Setter called for director")
self._director = new_director
@property
def cost(self):
print("Getter called for cost")
return self.__cost
@cost.setter
def cost(self, new_cost):
print("setter called for cost")
self.__cost = new_cost
m2 = Movie('Mr. C', 160, 100)
m2.cost
m2.cost = 105
m2.cost
Notice that the 'setter' is called here when the instance is instantiated. To see why this works we can take a look in the instance dictionary which should make it clear what is happening.
m2.__dict__
Out:
{'_director': 'Mr. C', '_Movie__cost': 160, '_revenue': 100}
Correct Way to Set Properties + deleter
Clearly we would want to set the properties using the same method. The example below also adds a deleter method that allows us to delete the properties.
class Movie:
def __init__(self, director, cost, revenue):
self._director = director
self._cost = cost
self._revenue = revenue
@property
def director(self):
print("getter called for director")
return self._director
@director.setter
def director(self, new_director):
print("setter called for director")
self._director = new_director
@director.deleter
def director(self):
print('calling the deleter on director')
self.director = None
@property
def cost(self):
print("Getter called for cost")
return self._cost
@cost.setter
def cost(self, new_cost):
print("setter called for cost")
self._cost = new_cost
@cost.deleter
def cost(self):
print("deleter called for director")
self._cost = None
@property
def revenue(self):
print("Getter called for revenue")
return self._revenue
@revenue.setter
def revenue(self, new_revenue):
print("setter called for revenue")
self._revenue = new_revenue
@revenue.deleter
def revenue(self):
print("deleter called for revenue")
self._revenue = None
m = Movie('Mr. A', 50, 200)
m.revenue = 100
del m.director
Computed Properties
We can also compute set properties based on computation with other attributes associated with the class/instance. Continuing with our movie example, we will create a new property:
profit = revenue - cost
class Movie:
def __init__(self, director, cost, revenue):
self._director = director
self._cost = cost
self._revenue = revenue
@property
def director(self):
print("getter called for director")
return self._director
@director.setter
def director(self, new_director):
print("setter called for director")
self._director = new_director
@director.deleter
def director(self):
print('calling the deleter on director')
self.director = None
@property
def cost(self):
print("Getter called for cost")
return self._cost
@cost.setter
def cost(self, new_cost):
print("setter called for cost")
self._cost = new_cost
@cost.deleter
def cost(self):
print("deleter called for director")
self._cost = None
@property
def revenue(self):
print("Getter called for revenue")
return self._revenue
@revenue.setter
def revenue(self, new_revenue):
print("setter called for revenue")
self._revenue = new_revenue
@revenue.deleter
def revenue(self):
print("deleter called for revenue")
self._revenue = None
@property
def profit(self):
print('calculating a computed property')
return self.revenue - self.cost
m = Movie('Mr. A', 50, 200)
m.profit
# returns 150
Lazy Evaluation of Computed Properties
Notice in the class above, when we created the profit property, we have to calculate it every time it is called. While this clearly wouldn't be such a big issue in the case of this example class, in some cases it may take a relatively long time to compute a property. Clearly we could also set the profit directly in the constructor with self.profit = self.revenue - self.cost , so bear in mind this example is simply to make a point using a simple case.
class Movie:
def __init__(self, director, cost, revenue):
self._director = director
self._cost = cost
self._revenue = revenue
self._profit = None
@property
def director(self):
print("getter called for director")
return self._director
@director.setter
def director(self, new_director):
print("setter called for director")
self._director = new_director
@director.deleter
def director(self):
print('calling the deleter on director')
self.director = None
@property
def cost(self):
print("Getter called for cost")
return self._cost
@cost.setter
def cost(self, new_cost):
print("setter called for cost")
self._cost = new_cost
self._profit = None
@cost.deleter
def cost(self):
print("deleter called for director")
self._cost = None
self._profit = None
@property
def revenue(self):
print("Getter called for revenue")
return self._revenue
@revenue.setter
def revenue(self, new_revenue):
print("setter called for revenue")
self._revenue = new_revenue
self._profit = None
@revenue.deleter
def revenue(self):
print("deleter called for revenue")
self._revenue = None
self._profit = None
@property
def profit(self):
if self._profit:
return self._profit
else:
self._profit = self.revenue - self.cost
return self._profit
m = Movie('Mr. A', 50, 200)
print(m.profit)
print(m.profit)
Take a look at the output this generates:
Getter called for revenue
Getter called for cost
150
150
Notice that the getter for revenue and cost is only called the first time the profit property is called, on the second call we simply return the computed property from the first call. Also take note that when we are using this method, we need to reset the _profit attribute to None when we use the setter on either cost or revenue. So although there is an increase in efficiency, it can also lead to unexpected behavior if we aren't careful.
Although this is a simple and rather silly example, it is intended to demonstrate the power of lazy evaluation. Which can be useful to increase code efficiency!
Summary
- Take care naming properties to avoid infinite recursion. Having a convention in which instance attributes in the constructor are prefixed with an _.
- Lazy evaluation can make code more efficient.