There is one tool provided with Python that makes prettier and cleaner writing easier: Python decorator.
Obviously they are not only used for that. There are other reasons like faster and more intuitive process of coding, but nevertheless, a decorator is an important tool to know when you dive deeper into Python.
In this article, I’m going to explain to you:
- what is a Python decorator,
- how to write it with and without arguments,
- what are the most popular Python decorators.
What is a Python Decorator?
Python decorator is virtually a function which returns another function. The simplest one you can think of is:
def simplest_decorator(func):
def wrapper():
func()
return wrapper
@simplest_decorator
def dummy_function():
pass
It doesn’t do a lot – it takes a function A and returns another function B which executes A, so it has no use to us at the moment. But this example shows us a general mechanism which is the most important in understanding why decorators are so useful.
How many times have you written the same piece of code in multiple functions to execute the same procedure? Maybe if you’re into machine learning, you generate predictions in the same way every time you run different models. Or maybe for each new data you have to save it or verify it in the very same way?
You already see what mechanism a Python decorator represents – it can be given any function, even different ones, and adds to them some repeatable functionality so we don’t have to write a piece of code in each of those functions.
This is achieved by using some wrapper
function inside – it executes our original function, but apart from that we can additionally execute whatever we want before or after the call (btw it doesn’t need to be call wrapper
, but let’s keep it this way to make it less confusing).
Python decorator example implementations
Let’s make it even more clear with the examples.
How to Write a Python Decorator without Arguments
Let’s think of a very simple logic we can add to any function – timing its execution.
from time import time
def calculate_time(func):
def wrapper():
start = time()
func()
print(f"Execution time of {func.__name__}: {time() - start}.")
return wrapper
We define our decorator calculate_time
, we define our wrapper
inside it and we say that:
- before
func
is called, we will check the current time and save it intostart
- and afterwards – we will see what is the difference between now and
start
Now when we decorate any function with calculate_time
, we’re gonna get:
@calculate_time
def mindless_for_loop():
for _ in range(10000): return 1
print(mindless_for_loop())
which returns
Execution time of mindless_for_loop: 3.0994415283203125e-06.
None
Great! There is one problem though. What happened to our return
? Why does mindless_for_loop
return None
instead of 1
?
Well it’s because our Python decorator doesn’t really know that it should return anything from it – we defined our wrapper
only to print things out. But there’s an easy fix for that – making wrapper
return what the original function would. Let’s tweak it a little bit.
from time import time
def calculate_time(func):
def wrapper():
start = time()
output = func()
print(f"Execution time of {func.__name__}: {time() - start}.")
return output
return wrapper
As you can see, now we stored the output
of func
and then returned it from wrapper
. What does the output look like?
Execution time of mindless_for_loop: 3.337860107421875e-06.
1
This is exactly what we were hoping for!
Unfortunately, there is another improvement to our Python decorator we need to add.
How to Write a Python Decorator with Arguments
One thing you can already see is that neither func
nor decorator has arguments to use. In fact, if we created a function with arguments and decorated it with calculate_time
it would raise an error!
@calculate_time
def simple_sum(a, b):
return a + b
simple_sum(5, 4)
# TypeError: wrapper() takes 0 positional arguments but 2 were given
Why is it doing so? When we decorate our function with a Python decorator, we actually pass its arguments to wrapper and it further decides if it should pass them to func
or not. This means that we need to create the ability for our wrapper to take any arguments. The easiest way to achieve it in a generalised way is to use *args and **kwargs.
from time import time
def calculate_time(func):
def wrapper(*args, **kwargs):
start = time()
output = func(*args, **kwargs)
print(f"Execution time of {func.__name__}: {time() - start}.")
return output
return wrapper
As you can see, now we stored the output
of func
and then returned it from wrapper
. What does the output look like?
@calculate_time
def simple_sum(a, b):
return a + b
print(simple_sum(5, 4))
we’re gonna get
Execution time of simple_sum: 2.1457672119140625e-06.
9
Nice!
The last thing we’re going to learn is to also add arguments to the decorator itself. Why? Well, maybe we want to use some information in the wrapper
itself like reference time to compare if we improved our execution time or not. Let’s tweak our Python decorator again.
from time import time
def calculate_time(reference_time=0.5):
def old_calculate_time(func):
def wrapper(*args, **kwargs):
start = time()
output = func(*args, **kwargs)
diff = time() - start
if diff < reference_time:
print("You improved your execution time!")
print(f"Execution time of {func.__name__}: {time() - start}.")
return output
return wrapper
return old_calculate_time
You can see that the only thing we did was to take our calculate_time
and put it inside another function. To keep the naming, we changed original calculate_time
into old_calculate_time
– this is the one we used in all our previous examples that takes func
as input, creates a wrapper
and returns it. Now, outside of it, we define a new calculate_time
that takes any arguments we want, and similarly return the old_calculate_time
.
When we run
@calculate_time(reference_time=0.3)
def simple_sum(a, b):
return a + b
print(simple_sum(5, 4))
we will get
You improved your execution time!
Execution time of simple sum: 0.0004658599035644531.
9
based on a value we gave as our reference_time
.
Known Python Decorators
Of course there are many decorators that already exist in Python, so you don’t have to define them on your own every time. I will now show you 2 examples that everyone should know about: classmethod
and staticmethod
.
When we use classes in Python, we define functions that take self
into account – it means they need the class to be already instantiated. What does it mean?
class Car:
def __init__(self, colour):
self.colour = colour
red_car = Car(colour="red")
In this example, instantiation is when we define red_car
by using the general concept of a Car
(class) and creating a specific example red_car
(instance).
Normally in the class, all functions are operating on self
. There are however cases when we want our class to have functions not dependent on any instance:
- we want it to operate on class (so our general concept and not example)
- we don’t want it to take either class or instance into account.
Let’s see it in the example to make it more clear.
StaticMethod Python Decorator
staticmethod
is the one which doesn’t care about class or instance. If we add
class Car:
def __init__(self, colour):
self.colour = colour
@staticmethod
def get_number_of_wheels():
return 4
we won’t need either class or instance to get the information about the number of wheels. We can run both
red_car = Car(colour="red")
print(red_car.get_number_of_wheels())
print(Car.get_number_of_wheels())
and each time get 4
as output.
ClassMethod Python Decorator
Now when we want to ignore the instantiation step, our way of doing so will be to use classmethod
. Let’s add to our class the ability to load it by reading a colour saved in some .txt file.
class Car:
def __init__(self, colour):
self.colour = colour
@classmethod
def load(cls, path_to_colour):
with open(path_to_colour, "r") as f:
colour = f.read()
return cls(colour)
Now we don’t have to pass the colour directly, but we can load it using
blue_car = Car.load("path/to/blue.txt")
print(blue_car.colour)
and get in return
blue
Summary
This article summarises a very powerful tool which is a Python decorator. By learning how to:
- define a decorator with and without return,
- with and without function arguments,
- with decorator arguments,
sky’s the limit to come up with new ideas.
And how would you use a Python decorator in your own work? What things would you automate with it? Together with context managers they make a hell of efficiency pair! Share below in the comments and I’ll be happy to discuss the ideas!