The next stop on our ELI5 tutorials is going to be decorates. These are another example of a very powerful bit of code that can be hard to understand. The quick description would be that these are ways to add code that runs before and after a function call. Read on for a more in-depth explanation.
Our objective for this is going to make code for messaging users about when a function starts and stops. This is a helpful feature to include in your code, especially if there are functions with a long execution time, as it would let users know what part of the script is currently running. Here are two simple functions to start:
def adder(a, b): print("starting adder") output = a + b print("finished adder") return output def multiplier(a, b): print("starting multiplier") output = a * b print("finished multiplier") return output
This code does the job fine, but there’s some repetition. As a general rule, you should avoid writing the same code multiple times as it can make the script difficult to read. In addition, it’d be nice if we could easily enable/disable the messaging which currently involves changing 2 lines of code per function. So, first lets define our decorator, which we will call “messenger”:
def messenger(function): def wrapper(*args, **kwargs): functionName = function.__name__ print(f"starting {functionName}") output = function(*args, **kwargs) print(f"ending {functionName}") return output return wrapper
Wow, that looks pretty weird, right? This is an ELI5 tutorial so I won’t focus much on why we do it like this, the simplest answer is that this is simply how decorators work.
Line by line, here is what is going on:
First, we define our decorator named “messenger” as a a function that takes another function as its argument.
Then, inside the decorator we define a “wrapper” function that takes *args and **kwargs. We need to use *args and **kwargs here in order to allow any possible function to be decorated.
The function.__name__ I use on the next line returns the name of the function as a string.
In the print statements I am using f strings for extra readability, the {functionName} part will be replaced by the string held by the functionName variable. This is only valid for python 3.6+
Then we get the output of the function by calling it with *args and **kwargs.
Finally, the wrapper function will return the output, then the messenger function returns the wrapper.
So, how do we use this? It is pretty simple, we just write @messenger on the line above the function definitions. The final code will look like so:
def messenger(function): def wrapper(*args, **kwargs): functionName = function.__name__ print(f"starting {functionName}") output = function(*args, **kwargs) print(f"ending {functionName}") return output return wrapper @messenger def adder(a, b): output = a + b return output @messenger def multiplier(a, b): output = a * b return output
Now, when we call adder and multiplier the @messenger decorator will print the “starting function” and “ending function” lines before and after it runs the function. While defining the decorator itself can be a bit tricky, once defined it is simple to add or remove it from your functions.
If you want to practice, I would suggest turning the timer function from the end of the *args and **kwargs tutorial into a decorator that prints execution times. This can be a very useful decorator to have as it can allow you to quickly add checking for slow function execution on a complex script.
I hope I was able to help you understand decorators a little better. Our next EL15 tutorial will be an introduction to objects in python.