The simplest and fastest way to distinguish junior and senior Python programmers in a technical interview is letting him or her write a decorator. Because mastering the decorator, which is the most magical Python feature of all time, is a milestone for a Python developer.
There are many tips and tricks worth to mention about decorators, but they are scattered around different books or tutorials and some of them may make beginners more confused. This is why I wrote this article.
This article will dive into all the core concepts, techniques and usages of Python decorators by 7 levels. If you understand half of them, reading Python programs containing decorators will be easy. If you understand all of them, designing and writing decorators in Python will be just a piece of cake. 🍰
Level 0: Understand the Basic Concepts and Usages
What is a decorator?
The decorator is just another functional programming feature in Python. It receives a callable (function, method or class), and returns a callable. Let’s see a simple example:
add_author(func) function is a simple decorator. It prints the author’s name before the received function runs.
If a function needs to use this decorator, we can add it following an “@” sign on the top of the function.
The use of the “@” sign looks confusing at first, but it’s just a syntax sugar of Python. We can apply the decorator as following as well:
get_title = add_author(get_title)
Results of the above method are totally the same as the “@” way:
Therefore, the “@” method is not intimidating at all. It just gives us a very intuitive and elegant option to apply a decorator to a function.
Why do we need decorators?
A decorator works like a reusable building block, we can apply it to a function when it’s needed without editing the function itself.
As the previous example shown, whenever we need to print the author name before a
get_title() function, we can just assemble this block (the
add_author decorator) into the function. No modification within the
get_title() function is needed. If we don’t need to print the author name in future, just delete or comment out the one line on the top of
Editing a function many times is bug-prone. Assembling decorators when it’s needed is elegant and bug-free.
— Yang Zhou
In a nutshell, the decorator gives us lots of flexibility and decouples the functionality methods and main methods. This great feature is used in many Python built-in modules and popular third-party modules.
Level 1: Wrap a Function Properly
A decorator is also called a wrapper in some materials. Because it can wrap a function and change its behaviours.
As to the example in level 0, our decorator just printed something before the
get_title() executed. Can we do more? Such as changing the returned title of
Yes, of course, if we know how to wrap the function as follows:
As demonstrated above, we define an inner function called
wrapper(), which wraps the received
func and adds three exclamation marks at the end of its result.
Basically, this example shows a common template for writing a decorator in Python. There are 3 steps:
- Receive a function as an argument
- Define a wrapper function which will do something with the received function
- Return the wrapper function
By the way, in the context of functional programming, our decorators which contain nested functions are named closures.
So far, we have already known the fundamentals of decorators. We are able to write some simple decorators as well. 🎉
Unfortunately, actual requirements can be very complicated and the above basics are not enough to design a robust decorator. The next levels will introduce more advanced techniques of decorators.
Level 2: Apply Multiple Decorators to a Function
Since a decorator is used as a functionality block, sometimes we would like to assemble many decorators to one function. How can we implement it?
Very easy, just put all the needed decorators on the top of the function as follows:
One important thing we should mind is the order of the used decorators. If we change the above example’s order, the results are different:
In fact, multiple decorators will wrap the function layer by layer from the bottom to the top. The above code is identical with the following:
Level 3: Wrap a Function That Receives Arguments
Our previous example program is good but not flexible enough. If we add an argument to the
get_title() function to let it receive a string as the title, it will be better.
But how to modify the decorators to adapt this change?
We can just let the wrapper function help us receive the argument:
The above code has already solved the problem, but it’s not very robust.
As mentioned previously, a decorator is a building block and can be added to other functions when it’s needed. However, we cannot make sure that all functions assembling the
add_author decorator have only one argument.
Therefore, our decorator is limited and cannot be applied to functions including many arguments.
Do we have to write many similar decorators that just have different arguments in the wrappers?
Fortunately, we don’t have to do that. The asterisks technique can make our lives easier:
As shown above, with the help of asterisks, our decorator can be assembled to functions without caring about how many arguments they will receive.
This is a popular and recommended way to design a decorator, since it makes a decorator more flexible and powerful.
Level 4: Write a Decorator That Receives Arguments
In fact, the examples in the previous level have another obvious bug:
Yang Zhou is not the author of “Harry Potter”!
We should let our decorator more flexible so it can receive an argument which represents the real author’s name of “Harry Potter”.
Now, things become a little complex: both the target functions and the decorator itself should receive arguments. The idea to implement this task is adding another layer outside the existed decorator.
As stated above, we just need to add one outer layer to the
add_author decorator to receive the argument.
The above program is identical with the following:
Level 5: Keep the Metadata of Original Functions
Until now, we have already designed a very flexible and robust decorator! But a true senior engineer will consider all details. Actually, the decorator function has a hidden side effect. Let’s see it by the following example:
The above results are not as expected. The name and doc of the
get_title function have been wrapped as well! This is the side effect of decorators.
To avoid this side effect, we can write some code like
wrapper.__name__ = get_title.__name__ manually. But there is an easier way:
As shown above, we can use the
wraps decorator in the
functools module, which will help us protect the metadata. As far as I am concerned, adding the
wraps decorator to every wrapper function is a good practice to avoid unexpected results.
Level 6: Keep It Simple — The Design Philosophy of Decorators
Congratulations! 🎉 🎉
If you arrive at this level, you have already understood, at least known, all the core techniques of Python decorators.
Last but not least, there is one philosophy worth to mention before you start to design decorators for your projects:
Keep it simple and stupid.
— A design principle noted by the U.S. Navy
The decorator is an elegant tool to help us write clean and neat Python code. But don’t overuse it or write a too complicated decorator. In my opinion, a decorator which has three layers of functions is enough, and assembling three decorators into a function is also enough.
As an old saying goes, beyond is as wrong as falling short. We should always mind the readability of our code and keep everything simple, even if we are using a complex feature.
Thanks for reading. If you like it, don’t forget to follow Yang Zhou to get more great articles about programming and technologies!