The Decorator Principle adds flexibility to customize functions in Python by adding extra behavior. It's implemented using callables and the '@' syntax.

The Decorator Principle

First of all, I will give you an introduction to the Decorator Principle. This principle is not limited to Python, you can use it in every other language as well. However, Python has some built-in syntax for decorators and the decorator principle fits the language design as well. Understanding this principle will make it much easier to familiarize yourself with decorators and understand their functionality.

To explain the decorator principle, I will take the menu of a restaurant as an example. We assume that every dish on the menu has a corresponding function or class in our code. Whether you use classes of functions with decorators may vary based on the programming language and architecture you are using, but the core concept of the pattern stays the same. To keep it simple, we will only use functions for this example.

At first, the menu only offers a burger and a steak. So we just have two functions handling both dishes:

Image

But the restaurant has a problem: there are customers who want custom versions of the dishes. Maybe someone wants another sauce or extra ingredients. To cover all these cases, we need functions for every single version of the dishes. As you can imagine, this is an absolutely terrible idea and design as it is inflexible and violates a lot of code principles like KISS or DRY.

Image

As you can see, it gets really messy. And this is where decorators come in! They provide a simple and flexible way to customize functions and classes by adding extra behavior. In our example, we create a decorator for each customization, which takes the original function of the dish and adds the special wish of the customer. By using this, we only have to code the special customer wishes once and separated!

Image

Then we can decorate our dish function according to the individual wishes of the customer. A function call with some decorators can then look like this:

Image

As the figure shows, every decorator calls the previous function and appends some additional content to it. Therefore, decorators depend on the function they are called on.

Implementation in Python

In Python, decorators are basically callables. A callable in Python can be a function or class, in general any object that you can call. However, the easiest and most common way to implement decorators in Python is to use functions. The special thing about them is that they take a function as an input argument and return a function as well.

def decorator(function):
# modify the function
# ...
return modified_function

Make sure to return only the function and don't call it like modified_function() for instance instead.

Since we want to create a decorator, we want to modify the behavior of the function given as input. A common way to do so is to create a new function that wraps the input function. In this wrapper function we can call the input function and do some other stuff too:

def double_result(function):
def wrapper(*args, **kwargs):
result = function(*args, **kwargs)
return result * 2
return wrapper

It is important to note that the wrapper function takes *args and **kwargs as arguments. If you are not familiar with this syntax, they take the normal and the keyword arguments passed into the function and put it into a list and a dictionary. Inside the wrapper, we reverse this by unpacking the arguments in the function call. In this way, we ensure that we get all the parameters that were passed to the original function and that we pass them on correctly.

Applying Decorators

To apply our decorator, we just use the @ syntax and attach it to a function. When the Python interpreter loads the code, it will call our decorator and pass the function automatically as the argument. Then our function gets replaced by the returned one from the decorator. With this in mind, you may realize that the syntax for decorators is just syntactic sugar and simply replace the function with a new one. So both of the following examples work the same:

from my_decorators import double_result
def calculate_answer(number: int) -> int:
return number - number
# replace function with decorated one
calculate_answer = double_result(calculate_answer)
# the code above is equal to:
@double_result
def calculate_answer(number: int) -> int:
return 1337 + number * 42

For this example, the output of the function without the decorator is the following:

>>> calculate_answer(7)
1631

If we append the double_result decorator to the function, we can see that the result is doubled.

>>> calculate_answer(7)
3262

Overview: Advanced techniques for Python Decorators

Decorators are very powerful, and with the basic implementation of the previous chapter, you are actually well served. Nevertheless, there are some advanced techniques that can be valuable in some cases. The following is a brief overview of what you can do. However, if you want to read more about specific topics, let me know in the comments!

Using functools.wraps

Decorators replace a function with another one, but what if we want the new function to have the attributes of the old one? If we take a closer look to the decorated function of the previous example, the name in the code stays the same, but the actual function name changed!

>>> calculate_answer.__name__
'wrapper'

In most the cases this should not matter, but if it does, we can use the wraps decorator from the functools module. This will copy the function attributes to the wrapper and prevent weird name confusion.

from functools import wraps
def double_result(function):
@wraps(function)
def wrapper(*args, **kwargs):
result = function(*args, **kwargs)
return result + 42
return wrapper

Decorators for Classes

Not only can you decorate functions, you can also decorate classes . This is rather used in other object-oriented languages like Java, but there are use-cases in Python as well. You can implement the Singleton pattern with class-decorators for instance.

Decorators with Arguments

Maybe you want to customize your decorators too, so you don't have to write multiple ones. In general, this is quite easy, but it also adds a new layer of complexity to your decorator. The solution is to simply wrap your decorator in another function! This function takes the arguments you need and returns the decorator. With the decorator syntax it's possible to call this function with your arguments and use the returned decorator instead. Python 3.9 also introduced more syntactic flexibility with the decorator syntax, you can take a look in our corresponding post if you are interested.

Stateful Decorators

A decorator can also handle state. There are multiple possible solutions to implement this. For instance, you can use instance objects of classes as decorators by implementing the __call__ method. However, since functions are also objects, you can simply add attributes to functions in order to add state.

I hope this post gave you a good introduction to decorators in Python and you learned how to create them on your own. If you have any thoughts, feedback or questions feel free to leave a comment below!