Properties in Python Classes

by John | November 25, 2020

 

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.

 

 


Join the discussion

Share this post with your friends!