Understanding Python decorators: six levels of decorators from beginner to expert
How decorators work, when to use them and 6 examples in increasingly complexity
Understanding Python decorators: Six levels of decorators from beginner to expert
Decorators are a very handy tool that can be used to change the behavior of a function without modifying the function itself. They allow you to easily add functionality without changing existing code. There are many use-cases (some of which we’ll get into today) like logging, performance checking, verifying permissions etc.
After this article you have a clear understanding how decorators work, how to apply them and when to apply them. We’ll start with the easiest, most basic example and then slowly work up our way to more complex ones. In the end we’ll have a function, decorated with multiple decorators of different types. Let’s code!
How decorators work
First we’ll specify a function that we want to decorate:
As you can see this function is called ‘sayhello’ and we can execute it by running sayhello()
(notice the () after the function name). This is different from print(sayhello)
; this returns the function name and the memory address where the function is stored:<function sayhello at 0x000001F64CA25A60>
The next step is to create a decorator:
As you see we create a function that expects another function. Then we define an inner function called wrapper in which we execute the received function. Then we return the inner function. The reason for defining the inner function is to handle arguments, we’ll see this in the next chapters.
We can use this decorator like this our_decorator(sayhello())
or add a little syntactic sugar and pass it like this:
Both methods are equivalent: we wrap sayhello
in our_decorator
.
Decorators in 6 steps of complexity
Now that we understand the way decorators and their syntax works we’ll go through 6 steps of decorators that show you all the possibilities and flexibility they offer.
1. The simplest decorator
To begin with we create one of the most basic examples of a decorator. We create a function called logging_decorator
that will be used to decorate other functions. First we check out the code and then go through:
We are re-using the old sayhello
fucntion as you can see. We decorate it with a function called logging_decorator
. As you see in lines 3–5 we can do something before and after we actually execute sayhello
so let’s print out the fact that we call this function for debugging purposes.
Also notice that we have access to the function as an object. This way we can call the function’s name in line 3 and 5. Executing the code below prints out the following:[LOG] calling sayhello
=== Hello from the function function
[LOG] called sayhello
Notice that our function is pretty simple; it doesn’t even return anything. In the next part we’ll up the complexity a bit more.
2. Passing arguments and a return value
In the previous part our decorator is merely executing the wrapped function. We’ll upgrade the code so that the function receives arguments and actually returns something. This is where the inner function called wrapper
comes in.
In the code above we decorate the multiply
function. This function needs two arguments. We modify the wrapper inside the decorator function with *args and **kwargs on line 2. Then we then pass them to the decorated function on line 4. On this same line we also receive the result of the decorated function that we then return on line 6. These minor changes make it so that we can receive a result from our target function:[LOG] calling multiply
=== Inside the multiply function
[LOG] called multiply
[result] 20
3. Multiple decorators
It is also possible to decorate a function with multiple decorators.
The code above first wraps multiply
in logging_decorator_2
and then wraps the whole bunch in logging_decorator_1
. It works from the target-function and moves upwards through all decorators. You can see in the output below in what order the decorators are called:[LOG1] calling decorator 1
[LOG2] calling decorator 2
multiply function
[LOG2] called decorator 2
[LOG1] called decorator 1
[result] 42
Calling the target function with our two decorators closely resembles calling it like the line of code below. Both are equivalent but in the line below the wrapper-like attributes are really clear:logging_decorator_1(logging_decorator_2(multiply(14,3)))
4. Passing arguments to decorators
In this part we’ll modify the logging decorator a bit so that it can receive arguments. We want to receive one argument that indicates whether debug-mode is on or off. If it’s on we print out the logging lines, otherwise we don’t.
The changes are in line 1; here we allow the decorator to accept a variable. Then in the wrapper we can access this variable and determine whether to print out the line or not. We can determine whether the debug-mode is on or off in line 13.
5 Decorator classes that have state
In this part we’ll create a decorator that counts the number of times the target function is called. In order to do this the decorator needs to conserve some state: it has to remember the number of times a function was called:
As you see we initialize the class with the target-function. Then, when the target-function is called we increment the call_count. This is how to use this decorator:
Executing this will produce the following result:Called multiply for the 1th time
Called multiply for the 2th time
Called multiply for the 3th time
Called multiply for the 4th time
res: (42, 70, 54, 8)
Keeping track of how often a function is called can be really convenient for limiting API calls e.g.
6. Interaction between decorator args and function args
In this last step we’ll create a decorator that can secure a function with a password. In the decorator argument we’ll define the password. We will check this with the password that’s provided through the target-function. Similar decorators are commonly used in providing security for API routes e.g.
Keep in mind that the example below just serves as an example; this is not a great example for secure code. Check out this article for more information on how to securely store confidential information in your code.
As you see we provide our valid password in the decorator on line 14. Then, inside the decorator, we take the password that’s provided through the target function from the args or kwargs on line 5. Then, on line 6, we check if the provided password is correct. Executing the code above results in the output below:You can only see this if you provided the right answer
Incorrect password
Conclusion
In this article I’ve tried to explain why and when you want to use decorators. Then we’ve gone through 6 increasingly complex examples with which I hope to have shown the flexibility that decorators can bring.
If you have suggestions/clarifications please comment so I can improve this article. In the meantime, check out my other articles on all kinds of programming-related topics like these:
- Why Python is slow and how to speed it up
- Advanced multi-tasking in Python: applying and benchmarking threadpools and processpools
- Write you own C extension to speed up Python x100
- Getting started with Cython: how to perform >1.7 billion calculations per second in Python
- Create a fast auto-documented, maintainable and easy-to-use Python API in 5 lines of code with FastAPI
- Create and publish your own Python package
- Create Your Custom, private Python Package That You Can PIP Install From Your Git Repository
- Virtual environments for absolute beginners — what is it and how to create one (+ examples)
- Dramatically improve your database insert speed with a simple upgrade
Happy coding!
— Mike
P.S: like what I’m doing? Follow me!