Have you heard about Python decorators? Not only do these special functions have a funny name, but their syntax differs from standard Python code (they attach to other functions through use of the @ sign).
No wonder many beginners avoid decorators at all costs! However, we’ve got good news for you. Decorating your functions isn’t as hard as it looks — and it results in cleaner, more modular programs with less boilerplate code.
What is a Python Decorator?
Decorators are a subset of “meta-programming” in Python, which refers to writing functions and classes that manipulate other code within the same program. Decorators in particular are great for wrapping existing code to improve readability and reusability in Python.
Essentially, they’re higher-order functions that take a function as an argument and return a different function. In doing so, they add new functionality to the input function without modifying its original definition. Decorators also contribute to code reusability by allowing one decorator to decorate multiple functions. This eliminates the need to copy and paste the same lines of code into all the function definitions.
Thus far, we’ve laid a conceptual foundation of how decorators work, but we really need to look at code to fully grasp their implications. First, it’s important to understand that functions are objects in Python and, as objects, we can store them in variables or pass them through function parameters. For example, given the following function definitions, if we run f2(f1) the program will output “Called f1”:
This is because when we call f2(f1), we’re passing the object representing f1, to f2. We then call that f1 object from within f2 to get the output we want.
Another aspect of Python functions essential to understanding decorators is the wrapper function. A decorator will take a function as its argument, but it’ll actually call that argument inside another function definition. That inner function is the wrapper. The role of the wrapper is twofold: it calls our argument function, and it adds some “decoration” to that function.
To trigger the wrapper function, we’ll need to call it somewhere. By returning it at the end of the decorator definition, we’re allowing our program to use it later, globally. To illustrate how our wrapper works, we need a function that’ll serve as the argument to the above decorator. Let’s use our previous definition of f1():
Now, if we were to call decorator(f1), what would our output be?
Nothing! That’s because we have not actually called wrapper, which in turn calls f1. By calling decorator(f1), we did, however, return a value equal to the function wrapper. Because of this, we can think of decorator(f1) as the decorator’s return value, wrapper, and consequently decorator(f1)() as wrapper().
Why is this relevant? Well, to do the actual “decorating” of our function f1 , we want it to act as would the wrapper function — printing “Hello,” executing the original function and then printing “Goodbye.” We can do that by reassigning our original function with the wrapper’s value. This is where the decorating happens:
This is essentially saying that the variable f1 now refers to the wrapper function instead of the original f1 function. Now, when we call f1(), we’re actually calling wrapper(). We’re thus able to give f1 the wrapper’s functionality without having to reuse the code in wrapper in f1’s definition.
Now, having associated the @ symbol with decoration, you may wonder where the decoration is actually happening. Well, the above line of code is longhand for using @decorator. So instead of having to write f1 = decorator(f1) after both functions’ definitions, all you would need to do after defining your decorator is:
By using @decorator right above your function definition, you remove extraneous code (you no longer have to write your decorated function’s name an extra two times) and you increase readability (even before reading the f1 definition, we can now easily tell that it’s decorated with decorator).
By now, we hope that the advantage of decorating your functions has become a bit clearer. But when was this design pattern implemented in Python, and why on earth does it look so strange?
A Short History of the Python Decorator
Being an object-oriented language, Python has always allowed the passing of functions as arguments to other functions. However, there was never any explicit decorator keyword until Python 2.4. Guido van Rossum, the author of the Python language, decided to use a Java-style @ symbol as the decorator keyword. This is also known as “pie-syntax,” because to some people, the “at” symbol looks like a little apple strudel.
Notably, while decorators are an object-oriented design pattern, they have somewhat different functionality in Python. In other languages like Java or C++, decorators modify an object’s behavior dynamically at run-time. In contrast, Python decorators may transform functions, methods and classes at declaration time, making them more powerful.
Decorating Functions With Parameters
When we first looked at how to code a decorator, we used a function that had no arguments and simply printed a statement. However, in practice, the functions we decorate often take arguments and return a value. So if we define a function that takes arguments, how do we adjust our wrapper definition and the function call inside the wrapper to reflect this reality?
This is where *args (arguments) and **kwargs (keyword arguments) come in. Adding these as parameters to our wrapper and the function call inside it means these functions will have a certain amount of arguments, however not needing to be specified when defining the wrapper:
Here we store the value returned by the wrapper function in a variable called value, which we return at the end of the wrapper definition. Doing this allows us to execute more functionality after calling the to-be-decorated-function func, rather than using return func(*args, **kwargs) in the middle of the wrapper, which would short-circuit the rest of its definition.
Python Decorator Applications
Now that we understand how Python developers approach decorators, let’s look at some of their common applications. Decorators are often used for login authorizations in Python frameworks, logging to files, timing function executions, synchronization and adding attributes to functions. You’ll frequently see decorators employed for memoization (that is, storing data that can be used at a later time).
Because decorators may decorate any function — whether part of a module, a class, or defined within another function — they have a wide variety of use cases in Python. Since we’ve gone over a few, let’s move on to actually building a decorator function.
How to Write Your Own Python Decorator
Say we want to annotate our functions, so that whenever a function is called, it prints out certain information about itself. We’ll make use of the fact that every Python object has a __name__ attribute that returns the object’s name.
We’ll first define the decorator before moving on to the functions that we want to decorate.
So much for our decorator. As we outlined earlier, the decorator acts at decoration time, essentially transforming the function in place. Therefore, we’ll need to define the wrapper function that will perform the transformation within the decorator itself. We’ll then return this new function, but without calling it just yet. This way, our decorated functions will output the changes at run-time.
So what happens when we call our two functions? You guessed it:
In addition to the output we defined for our functions, the decorator say_my_name causes them to print out their own names.
The example illustrates one of the main use cases for decorators: They are very handy when you systematically want to modify your functions with the same transformations. In web development for example, we can attach a decorator that checks for user authorization to all kinds of functionalities. In this way, we ensure that only authorized users can view certain content.
Did you know that you can decorate a function multiple times? Let’s write a second decorator as illustration. The builtin attribute __sizeof__ returns the size (in bytes) of an object in memory.
When we stack (or chain) decorators, the innermost decorator gets executed first. Thus, the above is equal to: say_my_name(print_my_size(say_goodnight)). Knowing this, can you guess the order in which our statements will be printed?
Oops, this is not what we wanted. While the inner decorator correctly prints the function’s size, the outer one prints the name of our wrapper function print_size! This is because we coded the print_my_size decorator to return the wrapped function instead of the original. Luckily, Python has a builtin module to deal with such higher-order functions.
We use functools.wraps to achieve the desired output. wraps is itself a decorator that lets our function keep its original attributes and methods even after decoration. It’s good practice to use @wraps in your decorators to prevent your functions from unwanted behavior, such as losing their docstrings. We redefine our decorator, passing the function argument f to @wraps:
We’ll modify our say_my_name decorator in the same way. Now we can decorate our function and get the information we’re after:
Our decorated function now prints both its own name and size in memory!
In this article, you took an in-depth look into decorators, touched on their history in Python, saw some of their applications and learned how to build your own.
To continue your journey as a Python programmer, check out our Introduction to Python Programming course, where you’ll work with Python features like decorators in greater detail.