Python is praised for its clarity and syntactic sugariness. In this article, I will teach you to use decorators in Python to make your code readable and clean.
What are decorators?
To understand what decorators are, you first need to be familiar with the way Python handles functions. From its point of view, functions are no different than regular objects: they have properties and can be reassigned:
def func():
print('hello from func')
func() # hello from func
new_func = func
new_func() # hello from func
print(new_func.__name__) # func
Moreover, you can pass them as arguments to other functions:
def func():
print('hello from func')
def call_func_twice(callback):
callback()
callback()
call_func_twice(func)
> hello from func
> hello from func
Now, to decorators. A decorator is used to modify the behaviour of a function or a class. The way this is achieved is by defining a function (decorator) that returns another function. This sounds complicated, but you will understand everything with this example:
Let’s go step-by step. Firstly, we define the logging_decorator
function on line 1. It accepts a single argument, which is the function we are trying to decorate. Inside, we define another function, the logging_wrapper
. The logging_wrapper
is then returned and is used in place of the original decorated function. On line 7 you can see how the decorator is applied to the sum
function. Later, on line 11, when we call sum
, it will not just call sum
: it will call the logging_wrapper
, which will log before and after calling the sum
. Let’s go through some more examples now.
Why do you need decorators?
It is simple: readability. Python is praised for its clear and concise syntax, and decorators are no exceptions. If there is any behaviour that is common to more than one function, you probably need to make a decorator. Here are some examples when they might come in handy:
- Check arguments type at runtime
- Benchmark function calls
- Cache function results
- Count function calls
- Check metadata (permissions, roles, etc.)
- Metaprogramming
- And much more…
Now I will list some code examples:
Decorators with return values
Suppose we want to know how long each function call takes. Also, functions return something most of the time, so the decorator must handle that as well:
You can see we store the returned value in result
on line 5. But before returning it, we have to finish timing the function. This is an example of behaviour that would not be possible without decorators.
Decorators with arguments
Sometimes we want a decorator which accepts values (like @app.route('/login')
in Flask):
You can see that in order to achieve it, we defined an extra function that accepts an argument and returns a decorator.
Decorating with classes
It is possible to decorate using classes instead of functions. The only difference is the syntax, so you do what you are more comfortable with. Here is the logging decorator rewritten using classes:
The upside is that you do not have to deal with nested functions. All you need to do is define a class and override the __call__
method.
Decorating classes
There may be times when you want to decorate each and every method in a class. You could always write it like this:
class MyClass:
@decorator
def func1(self):
pass
@decorator
def func2(self):
pass
But if you have lots of methods, this can get out of hand. Thankfully, there is a way to decorate the whole class at once:
Now, do not panic. This looks complicated, but this is the same logic. Firstly, we leave the logging_decorator
as is: it will be applied to all methods of a class. Then we define a new decorator log_all_class_methods
. It is like a regular decorator, but returns a class instead. The NewCls
, has a custom __getattribute__
: for all calls to the original class, it will decorate the functions with the logging_decorator
.
Built-in decorators
Not only can you define your own decorators, but there are some shipped in the standard library as well. I will list 3 I have worked most with:
@property
– a decorator from builtins, which lets you define getters and setters for class properties@lru_cache
– a decorator from thefunctools
module. It memorizes funcion arguments and return values, handy for pure functions, like thefactorial
.@abstractmethod
– a decorator from theabc
module. Indicates that the method is abstract and implementation details are missing.
Closing notes
Thank you for reading, I hope you liked my article. Stay subscribed for more Python content!
Resources
- PEP 318 — Decorators for Functions and Methods
- Higher-order functions and operations on callable objects