Python decorators provide a readable way to extend the behavior of a function, method, or class.
Decorating a function in Python follows this syntax:
@guard_zero
def divide(x, y):
return x / y
Here the guard_zero decorator updates the behavior of divide() function to make sure y is not 0 when dividing.
How to Use Decorators in Python
The best way to demonstrate using decorators is by an example.
Let’s first create a function that divides two numbers:
def divide(x, y):
return x / y
The issue with this function is it allows divisions by 0, which is illegal mathematically. You could solve this problem by adding an if check.
However, there is another option called decorators. Using a decorator, you do not change the implementation of the function. Instead, you extend it from outside. For now, the benefit of doing this is not apparent. We will come back to it later on.
Let’s start by creating the guard_zero decorator function that:
- Takes a function as an argument.
- Creates an extended version of it.
- Returns the extended function.
Here is how it looks in code:
def guard_zero(operate):
def inner(x, y):
if y == 0:
print("Cannot divide by 0.")
return
return operate(x, y)
return inner
Here:
- The
operateargument is a function to extend. - The
innerfunction is the extended version of theoperatefunction. It checks if the second input argument is zero before it callsoperate. - Finally, the
innerfunction is returned. It is the extended version ofoperate, the original funtion passed as an argument.
You can now update the behavior of your divide function by passing it into the guard_zero. This happens by reassigning the extended divide function to the original one:
divide = guard_zero(divide)
Now you have successfully decorated the divide function.
However, when talking about decorators, there is a more Pythonic way to use them. Instead of passing the extended object as an argument to the decorator function you can “mark” the function with the decorator using the @ symbol:
@guard_zero
def divide(x, y):
return x / y
This is a more convenient way to apply decorators in Python. It also looks syntactically nice and the intent is clear.
Now you can test that the divide function was really extended with different inputs:
print(divide(5, 0)) print(divide(5, 2))
Output:
Cannot divide by 0. None 2.5
(A None appears in the output because guard_zero returns None when y is 0.)
Here is the full code used in this example for your convenience:
def guard_zero(operate):
def inner(x, y):
if y == 0:
print("Cannot divide by 0.")
return
return operate(x, y)
return inner
@guard_zero
def divide(x, y):
return x / y
print(divide(5, 0)) # prints "Cannot divide by 0"
Now you know how to use a decorator to extend a function. But when is this actually useful?
When to Use Decorators in Python
Why all the hassle with a decorator? In the previous example, you could have created an if-check and saved 10 lines of code.
Yes, the decorator in the previous example was overkill. But the power of decorators becomes clear when you can avoid repetition and improve overall code quality.
Imagine you have a bunch of similar functions in your project:
def checkUsername(name):
if type(name) is str:
print("Correct format.")
else:
print("Incorrect format.")
print("Handling username completed.")
def checkName(name):
if type(name) is str:
print("Correct format.")
else:
print("Incorrect format.")
print("Handling name completed.")
def checkLastName(name):
if type(name) is str:
print("Correct format.")
else:
print("Incorrect format.")
print("Handling last name completed.")
As you can see, these functions all have the same if-else statement for input validation. This introduces a lot of unnecessary repetition in the code.
Let’s improve this piece of code by implementing an input validator decorator. In this decorator, we perform the repetitive if-else checks altogether:
def string_guard(operate):
def inner(name):
if type(name) is str:
print("Correct format.")
else:
print("Incorrect format.")
operate(name)
return inner
This decorator:
- Takes a function as an argument.
- Extends the behavior to check if the input is a string.
- Returns the extended function.
Now, instead of repeating the same if-else in each function, you can decorate each function with the function that performs the if-else checks:
@string_guard
def checkUsername(name):
print("Handling username completed.")
@string_guard
def checkName(name):
print("Handling name completed.")
@string_guard
def checkLastName(name):
print("Handling last name completed.")
This is much cleaner than the if-else mess. Now the code is more readable and concise. Better yet, if you need more similar functions in the future, you can apply the string_guard to those as well.
Now you know how decorators can help you write cleaner code and reduce unwanted repetition.
Next, let’s take a look at some common built-in decorators you need to know about in Python.
@Property Decorator in Python
Decorating a method in a class with @property makes it possible to call a method like accessing an attribute:
weight.pounds() ---> weight.pounds
Let’s see how it works and when you should use it.
Example
Let’s create a Mass class that stores mass in kilos and pounds:
class Mass:
def __init__(self, kilos):
self.kilos = kilos
self.pounds = kilos * 2.205
You can use this class as follows:
mass = Mass(1000) print(mass.kilos) print(mass.pounds)
Output:
1000 2205
Now, let’s modify the number of kilos, and see what happens to pounds:
mass.kilos = 1200 print(mass.pounds)
Output:
2205
Changing the number of kilos did not affect the number of pounds. This is because you did not update the pounds. Of course, this is not what you want. It would be better if the pounds property would be updated at the same time.
To fix this, you can replace the pounds attribute with a pounds() method. This method computes the pounds on-demand based on the number of kilos.
class Mass:
def __init__(self, kilos):
self.kilos = kilos
def pounds(self):
return self.kilos * 2.205
Now you can test it:
mass = Mass(100) print(mass.pounds()) mass.kilos = 500 print(mass.pounds())
Result:
220.5 1102.5
This works like a charm.
However, now calling mass.pounds does not work as it is no longer a variable. Thus, if you call mass.pounds without parenthesis anywhere in the code, the program crashes. So even though the change fixed the problem, it introduced syntactical differences.
Now, you could go through the whole project and add the parenthesis for each mass.pounds call.
But there is an alternative.
Use the @property decorator to extend the pounds() method. This turns the method into a getter method. This means it is still accessible similar to a variable even though it is a method. In other words, you do not need to use parenthesis with this method call:
class Mass:
def __init__(self, kilos):
self.kilos = kilos
@property
def pounds(self):
return self.kilos * 2.205
For example:
mass = Mass(100) print(mass.pounds) mass.kilos = 500 print(mass.pounds)
Using the @property decorator thus reduces the risk of making the old code crash due to the changes in syntax.
@Classmethod Decorator in Python
A class method is useful when you need a method that involves the class but is not instance-specific.
A common use case for class methods is a “second initializer”.
To create a class method in Python, decorate a method inside a class with @classmethod.
Class Method as a Second Initializer in Python
Let’s say you have a Weight class:
class Weight:
def __init__(self, kilos):
self.kilos = kilos
You create Weight instances like this:
w = Weight(100)
But what if you wanted to create a weight from pounds instead of kilos? In this case, you need to convert the number of kilos to pounds beforehand:
pounds = 220.5 kilos = pounds / 2.205 w2 = Weight(kilos)
But this is bad practice and if done often, it introduces a lot of unnecessary repetition in the code.
What if you could create a Weight object directly from pounds with something like weight.from_pounds(220.5)?
To do this, you can write a second initializer for the Weight class. This is possible by using the @classmethod decorator:
class Weight:
def __init__(self, kilos):
self.kilos = kilos
@classmethod
def from_pounds(cls, pounds):
kilos = pounds / 2.205
return cls(kilos)
Let’s take a look at the code to understand how it works:
- The
@classmethodturns thefrom_pounds()method into a class method. In this case, it becomes the “second initializer”. - The first argument
clsis a mandatory argument in a class method. It’s similar toself. Theclsrepresents the whole class, not just an instance of it. - The second argument
poundsis the number of pounds you are initializing theWeightobject form, - Inside the
from_poundsmethod, thepoundsare converted tokilos. - Then the last line returns a new
Weightobject generated frompounds. (cls(kilos)is equivalent toWeight(kilos))
Now it is possible to create a Weight object directly from a number of pounds:
w = Weight.from_pounds(220.5) print(w.kilos)
Output:
100
@Staticmethod Decorator in Python
A static method in Python is a method tied to a class, not to an instance of it
A static method could also be a separate function outside the class. But as it closely relates to the class, it is placed inside of it.
A static method does not take reference argument self because it cannot access or modify the attributes of a class. It’s an independent method that works the same way for each object of the class.
To create a static method in Python, decorate a method in a class with the @staticmethod decorator.
For example, let’s add a static method conversion_info into the Weight class:
class Weight:
def __init__(self, kilos):
self.kilos = kilos
@classmethod
def from_pounds(cls, pounds):
kilos = pounds / 2.205
return cls(kilos)
@staticmethod
def conversion_info():
print("Kilos are converted to pounds by multiplying by 2.205.")
To call this method, you can call it on the Weight class directly instead of creating a Weight object to call it on.
Weight.conversion_info()
Output:
Kilos are converted to pounds by multiplying by 2.205.
Because the method is static, you can also it on a Weight object.
Conclusion
In Python, you can use decorators to extend the functionality of a function, method.
For example, you can implement a guard_zero decorator to prevent dividing by 0. Then you can extend a function with it:
@guard_zero
def divide(x, y):
return x / y
Decorators are useful when you can avoid repetition and improve code quality.
There are useful built-in decorators in Python such as @property, @classmethod, and @staticmethod. These decorators help you make your classes more elegant. Under the hood, these decorators extend the methods by feeding them into a decorator function that updates the methods to do something useful.
Thanks for reading.
Happy coding!