Decorators
To better understand Decorators, consider a scenario of painting a house. If the house is already painted in one color let’s say white, we also have the option of adding another color of paint to a specific part of the house, without having to remove the entire paint of the house.
Likewise, Decorators give us the ability to add extra functionalities or features to our function without having to modify the original function. The original function which we do not want to modify is called the ‘Wrapper Function’ and the function which we use to add more functionalities to our original function is called ‘Decorator Function’.
We cannot call a decorator function directly instead we must use a closure.
Consider an example:
# Our decorator function takes another function as an argument def decorator_function(function_name): # Our wrapper function def wrapper_function(): print("From Wrapper Function") # Calls the required function with the parameter name function_name() # Return the inner function without calling it return wrapper_function def custom_function(): print(f"Working with Custom Function") custom_function() print("") # Creating a closure decorator_object = decorator_function(custom_function) decorator_object()
The output we get:
First, we called the function to see what we would get as the output without a Decorator. We can see that it only called its print statement. However, when we used a closure to call our Decorator, we that an additional print statement from the wrapper class was called before our wrapped function.
We first create an object of the decorator function called “decorator_object” with the original function called “custom_function” given as an argument to it. The decorator function takes the name of a function as an argument, then the wrapper function provides the additional feature, which in this case is to print a sentence and then call the original function.
Python also provides a way to automatically call our decorator by creating a closure without having us manually create one. We can achieve this using the ‘@’ symbol followed by the name of the decorator. Consider how we can do this using our previous example:
# Our decorator function takes another function as an argument def decorator_function(function_name): # Our wrapper function def wrapper_function(): print("From Wrapper Function") # Calls the required function with the parameter name function_name() # Return it without calling the function return wrapper_function # Calling our decorator function @decorator_function def custom_function(): print(f"Working with Custom Function") # Now we just have to call our function custom_function()
This gives us the same output as when we used a closure to call our decorator function. Thus using ‘@’ eliminates the need to manually create a closure.
Property Decorator
Let’s say we created a website for a community of gamers and this takes in a couple of details as information. We use a class to store the information of different users, and within the constructor, we create a variable that uses the information provided to assign them a role in that community.
Suppose a user entered the wrong information and wanted to change it, we can achieve this by updating the object but it won’t be the preferred result. Consider the code for such a scenario:
class Gamer: def __init__(self, device, genre): self.gamer_device = device self.game_genre = genre self.gamer_role = self.gamer_device + " Advisor" def update_bio(self): return "I am a " + self.gamer_device + " gamer who plays " + self.game_genre + " games." # Creating Object gamer1 = Gamer(device="PC", genre="Strategy") print("gamer1 Role is:", gamer1.gamer_role) print("Your bio is:", gamer1.update_bio()) # Updating the gamer object gamer1.gamer_device = "PlayStation" print("\ngamer1 Role is:", gamer1.gamer_role) print("Your bio is:", gamer1.update_bio())
When we run this, we see:
We see that even though we have updated the gamer’s device variable, that change is not reflected in the gamer’s role variable. This is because the value was initialized when the object’s constructor was called. The first solution to this problem would be to create a simple function instead of creating it in the constructor. A function such as:
def gamer_role(self): return self.gamer_device + " Advisor"
Now to view the change, we would simply have to call the object’s function as:
print("gamer1 Role is:", gamer1.gamer_role()) print("Your bio is:", gamer1.update_bio())
When we run this, we get:
While this might work as a solution, there is a notable difference. The gamer’s role is no longer an attribute of the class but is now a function that we need to call. Now if the code has gone out to thousands of developers, they would now have to individually change each instance of that attribute to a function.
To make changes to a value initialized in the constructor and still retain it as an attribute we need to use the ‘Property Declarator’ (@property). The Property Declarator allows us to modify values we initialized in our constructor. Now let’s modify our class using the @property Decorator.
class Gamer: def __init__(self, device, genre): self.gamer_device = device self.game_genre = genre @property def gamer_role(self): return self.gamer_device + " Advisor" def update_bio(self): return "I am a " + self.gamer_device + " gamer who plays " + self.game_genre + " games." # Creating Object gamer1 = Gamer(device="PC", genre="Strategy") print("gamer1 Role is:", gamer1.gamer_role) print("Your bio is:", gamer1.update_bio()) # Updating the gamer object gamer1.gamer_device = "PlayStation" print("\ngamer1 Role is:", gamer1.gamer_role) print("Your bio is:", gamer1.update_bio())
We get the same output as before:
However, the biggest difference is that we can now call a gamer’s role like it is an attribute instead of a function.
What have we learned?
- What are Decorators?
- What is a wrapper function?
- What symbol simplifies the process of working with Decorators?
- What is the Property Decorator?
- What is a use case for @property?