avatarYong Cui

Summary

The article provides an in-depth exploration of Python decorators, detailing five advanced features that enhance their functionality, including support for different function signatures, preservation of function metadata, decorators with arguments, the use of multiple decorators, and the implementation of class-based decorators.

Abstract

Python decorators are powerful tools that modify the behavior of functions without altering their core operations. The article begins by establishing the fundamental concept of decorators as functions that add functionality to other functions, similar to adding coatings to a plain donut. It then delves into more sophisticated aspects of decorators, such as handling various function signatures using *args and **kwargs, maintaining the original function's metadata with the wraps decorator from the functools module, and creating decorators that accept arguments to provide user-specified behavior. The article also discusses the application of multiple decorators to a function, illustrating how the order of decorators affects the outcome, and introduces the concept of class-based decorators, which leverage the __call__ method to make instances callable as decorators. These advanced features enable developers to craft versatile and robust decorators that can significantly aid in routine programming tasks.

Opinions

  • The author emphasizes the importance of using *args and **kwargs in decorators to ensure compatibility with a wide range of function signatures.
  • It is suggested that the wraps decorator is essential for preserving the original function's docstrings and other metadata, which is crucial for code readability and maintainability.
  • The article advocates for the use of decorators with arguments to allow for flexible and customizable behavior, catering to specific user preferences or business requirements.
  • When using multiple decorators, the author points out the significance of their application order, which can lead to different outcomes based on proximity to the decorated function.
  • The exploration of class-based decorators is presented as a more complex yet powerful alternative to function-based decorators, offering the potential for even greater customization and functionality.
  • The author challenges readers to implement their own class-based decorators, highlighting the versatility and potential of this approach while acknowledging the additional complexity involved.

Python Decorators — 5 Advanced Features to Know

Take advantage of Python decorators in your project

Photo by Mantas Hesthaven on Unsplash.

Decorators are functions that modify other functions’ behaviors without changing their core operations. As indicated by the name, decorators only decorate other functions. You can think of other functions as plain donuts, and what decorators do is apply different coatings to the donuts. No matter what flavor you have (the decorators), donuts (the decorated functions) are still donuts.

The following code shows you a basic decorator that logs the elapsed time of a function call. In essence, the decorator function accepts another function (i.e. the to-be-decorated function) as its input argument. It defines an inner function that actually provides decoration activities and returns the inner function as the output. To use the decorator, you simply place the decorator function name with an @ sign prefix above the function that you want to decorate with the decorator function.

Now that you have a good understanding of the most basic form of decorators, it’s time to gain some more in-depth knowledge about them.

1. Support Different Function Signatures

There is a problem with the code snippet above: It assumes that the decorated functions don’t require any input arguments (Line 7). If we use the decorator in its current form with a function that takes an argument, it won’t work as you may expect:

To address this issue, we should consider using *args and **kwargs with the decorator definition. These two terms are used to denote an undetermined (zero to more) number of positional and keyword arguments in functions. In other words, they can capture all kinds of function signatures. Let’s see the modified version and its improved compatibility:

As you can see, the biggest change is that instead of assuming the function takes no arguments, the revised version supplies the function call with *args and **kwargs such that the decorator is more versatile now.

2. Wrap the Decorated Function

Some people may not know that the decoration will by default mess up the metadata of the decorated function, such as docstrings. Let’s look at the behavior of the current decorator:

As you can see, the docstrings show the inner function that we defined in the decorator function but not the decorated function say_hello. Under the hood, it’s all because the decoration process is to create a closure from the decorator function. In essence, the process of decoration is equivalent to calling say_hello = logging_time(say_hello). Thus, it’s not surprising that you’re getting the inner function’s docstrings with the decorated function.

To solve this problem, we can use another decorator function (wraps) that is shipped in the standard Python library, as shown below:

  • Line 2: We import the wraps decorator function from the functools module.
  • Line 6: We use the wraps decorator to decorate the inner function by wrapping the to-be-decorated function (the func argument).
  • Lines 21-23: The decorated function now has the correct docstrings.

Besides the benefits of passing the expected docstrings for the decorated functions, the wraps decorator is necessary to have the decorated function show correct function annotations (e.g. argument types) and support pickling for data preservation.

3. Define Decorators With Arguments

So far, our decorators have their decoration functionalities fixed. What if we want our decorators to behave differently based on the user’s preferences? In this case, we can consider defining decorators that accept arguments. Let’s continue with the decorator example of logging the elapsed time of functions. Suppose a trivial business need: Our decorators display the time in the unit that’s specified by the user (either in milliseconds or seconds). The following code shows you a possible solution:

As you can see, to allow the decorator to accept the unit parameter, we need to create another layer outside the decorator that we defined earlier. Let’s see whether it works as we’re expecting:

  • We use two different settings for the decorator and both work as expected.
  • The reason for adding another layer to get the decorator to accept arguments is that the decoration process is chaining the function call. Calling logging_time(“ms”) would allow us to get the logger function, which has exactly the same function signature as the decorator function that we defined earlier.

Please note that the current definition of the decorators requires that we specify the unit for the decoration. If you want to make your arguments optional, it needs extra work. You can find the pertinent discussion in my earlier article on this topic.

4. Multiple Decorators

The examples above have only used one decorator to decorate other functions. However, it’s possible to use multiple decorators to decorate functions at the same time. To do that, we can simply stack the decorators above the to-be-decorated function. The following code snippet shows you a trivial example:

The code above shows you that we created another decorator that simply calls the decorated function twice. Notably, we defined two functions that are decorated by two decorators. However, we applied the decorators in a different order, which will cause distinct effects:

  • We notice that the say_hi function gets called twice and the time is only logged once. Meanwhile, the say_hello function gets called twice and the time is also logged twice.
  • When we have multiple decorators, the order of applying the decorators is based on proximity. In other words, the one that is right above the decorated function is to exert decoration first and so on. This is why the say_hi function’s time gets logged once — because the logging_time decorator is applied last. By contrast, the repeat decorator applies to the function that has already been decorated by logging_time, and thus the time for the say_hello function is logged twice.

5. Class-Based Decorators

We have been saying that decorators are functions. To be precise, these are higher-order functions, which means that these functions use other functions as input and/or output arguments. However, do you know that decorators can be implemented as a class? With the possibility of having decorators as classes, we should say that decorators are callables. In a previous article, I introduced callables. Please feel free to take a look at it if you don’t know about callables.

The following code shows you how we can define a decorator using a class:

  • The example shows you not the most basic form of decorators but decorators that can take arguments.
  • I’ll leave you the challenge of creating a class that serves as a decorator. The basic principle of implementing decorators using classes is the same as with regular decorator functions. You can think of classes as functions because the class above becomes callable by implementing the __call__ method.
  • As you can see, by specifying the number of repeat times, the decorated function works as expected.

Although it’s possible to implement decorators using classes, it can be more complicated than introduced here if you want your decorators to be fully versatile. The example given is just to provide you a proof of concept. If you want to use these decorators with methods that are defined in a class, you have to consider the positional arguments related to the class (i.e. cls) or the instance (i.e. self). For more in-depth knowledge, you can refer to some good discussion on Stack Overflow.

Conclusion

In this article, we first reviewed the basic form of decorators and then learned five more advanced features of decorators. When you have a good understanding of decorators, you can define some custom ones (e.g. logging time, type checking) that will help you with routine work.

Programming
Python
Technology
Artificial Intelligence
Data Science
Recommended from ReadMedium