Python Decorators - A Deep Dive
Decorators are the way to give super powers to your Python functions. It takes DRY principle up a notch
A decorator is a function that takes another function as input and returns a function as output. It is one of Python's most powerful features. A decorator can add functionality to a function without modifying the function itself, such as logging, caching, and other functionality.
On the 1st look, decorators are confusing. But, once you understand how they work, you will never write Python code without them. Often times, even people who can't understand the concept of decorators can understand the use cases of decorators.
This post has two parts.
Understanding the concept of decorators
Real-world examples of decorators from famous Python libraries
What is a function?
If you know a little bit of programming, you know what a function is. Functions encapsulate a piece of code that can be reused.
We all understand this. But do you know functions are objects in Python? Try the following in a Python shell.
Given this understanding, we can pass functions as arguments to other functions.
In the above example, we are passing a add
as an argument to logger
and using it to log a variety of things. This intuition of functions' ability to be passed around is the basis of decorators.
What is a decorator?
Unlike the above logger function, a decorator should take a function as an argument and return a function. An updated logger function would look something like
Let's take a deeper look at the logger
function.
It takes a function as an argument
func
.Inside that is a function
wrapper
which takes any arg.You can see that the wrapper inner function is returned.
The wrapper function is called with the arguments passed to the original function
add(1, 2)
, logs, and returns the value.
Decorator Syntax
The above example can be written in a Pythonic way using the decorator syntax. Rather than doing
We can write
Every time you see @<some-name>
in Python above a function, it is a decorator. The above example is equivalent to
Decorators with Arguments
The above example is a simple decorator. But what if we want to pass arguments to the decorator? Let's say we want to log only if the function returns a value greater than 10. We can do that by passing arguments to the decorator.
The above example has 3 functions
logger
- function to register decorator argumentsdecorator
- The decorator itselfwrapper
- The wrapper function that runs before and after the original function
How does it work?
When the code is run, and the interpreter sees
@logger(min_return_val=10)
it evaluates the function call. So, it callslogger(min_return_val=10)
and returns thedecorator
function.The
decorator
function is called with the function to be decoratedadd
and returns thewrapper
function.The
wrapper
function is called Theyadd(1, 2)
is called and executes the logic inside the wrapper andadd
function
Decorators in Famous Python Libraries
For most of my students, the above example doesn't do justice until they go through the following. So, let's look at some of the decorators in famous Python libraries.
Django @login_required
Django's @login_required
decorator ensures that a view is only accessible to logged-in users.
Internally @login_required
is implemented as a function that takes a view function as its argument and returns a new view function that wraps the original view function. The new view function checks if the user is logged in and, if they are, calls the original view function. If they are not logged in, they are redirected to the login page.
Flask @app.route
Flask's @app.route
decorator is used to register a view function for a given URL rule. The view function is called whenever a request with the matching URL is received.
Internally @app.route
is implemented as a function that takes a URL rule as its argument and returns a new function that takes a view function as its argument and returns a new view function that wraps the original view function. The new view function registers the original view function for the given URL rule.
You can read my work on Building your own Flask to learn more about how Flask works under the hood.
Similarly, there is @app.before_request
and @app.after_request
which are A used to register functions to be called before and after each request, respectively.
timeit @timeit.timeit
The @timeit.timeit
decorator is used to time a function. It is used to measure the execution time of a function. This is the most used example to explain decorators.
The output of the above code will be something like this:
If you implement @timeit.timeit
as a function, it will look something like this:
Unittest
The @unittest.skip
, @unittest.skipIf
, and @unittest.skipUnless
decorators are used to skip tests. They are used to skip tests that are not applicable in certain situations.
Pytest
My favorite decorator of all time. The @pytest.mark.parametrize
decorator is used to parametrize tests. It is used to run the same test with different arguments.
You can read my work on Building your own pytest to understand how it works.
Celery
The @celery.task
decorator is used to create a celery task. It is used to create a celery task from a function.
Internallcreatemplemented as a function that takes a function as its argument and returns a new function that wraps the original function. The new function creates a celery task from the original function.
This example needs redis to run. You can install redis using
pip install redis
.
lrucache
The @functools.lru_cache
decorator is used to cache the result of a function. It is used to cache the result of a function so that the next time the function is called with the same arguments, the cached value is returned instead of calling the function again.
Retry
The @retrying.retry
decorator is used to retry a function. It is used to retry a function until it succeeds.
Your Turn
I hope that clarifies the concept of decorators. Now it's your turn to try it out. Try implementing the following to understand and make decorators a muscle memory.
Implement a decorator that handles exceptions on any given function.
Implement a decorator that retries a function until it succeeds. Fail after 3 times.
Implement a decorator that logs the time taken for a function to run.
Do you still have questions? Write them to me on Twitter.
Last updated