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
operate
argument is a function to extend. - The
inner
function is the extended version of theoperate
function. It checks if the second input argument is zero before it callsoperate
. - Finally, the
inner
function 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
@classmethod
turns thefrom_pounds()
method into a class method. In this case, it becomes the “second initializer”. - The first argument
cls
is a mandatory argument in a class method. It’s similar toself
. Thecls
represents the whole class, not just an instance of it. - The second argument
pounds
is the number of pounds you are initializing theWeight
object form, - Inside the
from_pounds
method, thepounds
are converted tokilos
. - Then the last line returns a new
Weight
object 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!