Decorators are a powerful and versatile feature in Python, but they can seem daunting and opaque to newer users. In this post, I'll explain how they work and give a few examples of when you might employ them.
Before I discuss decorators, it's important to highlight the fact that functions are first-class objects in Python, meaning that they can be stored in variables and data structures or even passed as arguments to other functions. Below are a few examples:
Here I've defined a few simple functions and put them in a list. It's important to note that I'm not invoking the functions within the bracket notation of function_list
. Doing so, by using parentheses like this: duck_talk()
, would call the function and put whatever it returns into the list. What I want instead is to contain a reference to the function itself so that I may call it later, just like in line 11. I reference the first item in the list using its index with function_list[0]
and then include the parentheses to finally call our duck-based function. The result of running the program above then looks like this:
I can assign functions to variables as well:
The output remains the same as our previous example:
Check out what happens when I start passing functions as arguments to other functions:
My add_bark
function is a simple one. It takes a string and adds a bark to the end. The good stuff here is in the say_hello
function. It is designed to take a function as its argument. In the body of say_hello
, we call that function, pass the string "Hello" as its one argument, save the result in our variable greeting
, and then print the contents of greeting
. I'd like to point out that because of the way I designed the say_hello
function, it can only accept as its argument functions that can take one string for their argument, since that's the way the fn
parameter is used in the body.
On line 11, I call the say_hello
function and pass add_bark
as its argument. As a result, the program outputs a greeting with a bark:
To unlock the power of decorators, we need to not only take in functions as arguments but return them as well. The example below brings together all the main ingredients for a decorator:
I want to highlight what's going on in the make_enthusiastic
function. Here, I defined another function within the main function body and then returned that function. Again, note that I did not call result_function
on line 12. What make_enthusiastic
will do is return a function that when called takes a string as its argument and returns the output of calling fn
with that string plus an exclamation point. On line 14 I call make_enthusiastic
, passing the add_bark
function and store it in the variable enthusiastic_dog
. Because make_enthusiastic
returns a function that accepts a string as its argument, this means that enthusiastic_dog
is also a function that accepts a string. Up on line 6 I call enthusiastic_dog
, passing "Hello" as its argument. The final output looks like this:
The "Hello" was passed to enthusiastic_dog
on line 6, ', bark' came from the body of add_bark
on line 3, and the exclamation point originated from line 11. By passing functions around as arguments and storing them as variables, we were able to alter their behavior in ways that will ultimately prove quite useful.
Here's the good news: In the example above, we essentially created a decorator function, just without the syntactic sugar that Python provides to make things a little cleaner and easier. Below I have refactored the code to make use of Python's decorator syntax:
The function make_enthusiastic
is unchanged. It still takes in and returns functions. Instead of having to call make_enthusiastic
with add_bark
as its argument and storing the result in a variable, we just use the @make_enthusiastic
decorator in front of the add_bark
function definition. This means that whenever we call add_bark
, it will be "wrapped" by the function make_enthusiastic
. Decorators allow us to modify behavior or add side effects to existing functions without having to change the functions themselves. Running the program above gets us the same end result as the previous code:
I'd like to look at a more concrete example of how decorators can make life easier for a Python programmer. The snippet below would be at home in a program designed to output information to the command line:
I added some styling from the Rich Python library so the output is as folows:
This is all well and good, but perhaps the rest of the program outputs quite a bit of text and I want a way to let certain functions' results stand out. If it's just one function, that wouldn't take to long, but if there are several different types of print functions, it begins to seem arduous. Decorators to the rescue! I can define one decorator function and use the @decorator syntax to attach it to whatever functions I want. Here's how I would create a decorator function to print a row of colorful stars before and after a function's output in order to highlight it:
Note that I am calling the function I define and return wrapper
. This is a common convention. We'll get back to the print_list
function in a moment, but first let's see this decorator in action on a more simple function, such as one that just prints a welcome to the console:
By applying the @visual_flair decorator above we'll see a more eye-catching result upon running the program:
Simple decorators like this one also come in handy for debugging and testing - imagine if instead of a row of stars, the decorator function logged certain variables or other relevant information. Since the decorators are so easy to add and remove from a function, they can work like specialized breakpoints right where you need them.
I've got one more detail to explain about decorators: how to apply decorators to functions that accept arguments. In our barking example we hard-coded come of this behavior, but the print_list
function wouldn't be too useful if it printed the same list every time. This is where the *args feature of Python comes in handy to forward a variable number of arguments from the wrapper to the decorated function. Here's how it looks:
The *args feature (and its sibling *kwargs) are a topic worthy of much further discussion, but for our decorator-based purposes all you need to know is that by entering *args as an argument for both the wrapper and the call to the decorated function, you can make sure the information you wanted to pass to the original function can still make it through. In this case, it is the list of names to be printed. Now that the decorator has been applied to our print_list
function we'll see the same highlighting applied:
Try decorating some of your own functions to get a handle on this useful feature. Good luck!