Python: Decorator @

Sean
NTUST-AIVC
Published in
7 min readAug 7, 2022

Decorator is a design pattern in Python that helps you add additional features to an original function without changing its structure.
In the following article, I will introduce decorator to you.

Contents

Note: In this article, we have multiple times talked about the “return object”, which means a return value is an object. The purpose of calling the “return object” is hope to emphasize that object can also be flexible to pass between functions.

Prerequisite knowledge

Before you dig deeper into Decorator, you shall first know two basic knowledge in Python:

First-class Function

Function in Python belongs to First-class Function. It means that functions in Python can be treated as variables. It can be passed into other functions as an argument and also can be treated as a return object assigned to other variables.

Closure

Another basic concept of decorator is related to Closure. Closure involves two basic elements :

  • There is a function that defines another one in it and returns the created one as a returned value.
  • The created function must refer to a local variable in the enclosing function which defines the new function in itself.

This two basic concepts aren’t the main point in this article. If you are interested in it, I list two websites in the end of article which talk about these.

Introduction of Decorator

Let’s just see an example:

pic 1

In this example, I create two functions, teacher() and student() . They will print their duty individually. If you want to check the starting time of the function, you may import time module to show when they operate. Add codes into the definition as follows:

pic 2

When other functions also need to show the starting time, they may also add the same codes into the definition. It is so inefficient and makes your code redundant. If so, you can try using a decorator to help you streamline the content :

pic 3

As you can see, I defined a new function show_time. show_time imports time module to use its function. Because of the properties of Closure, the enclosed function can access local variables of enclosing functions even though enclosing functions have done execution.

Let’s see Line 15, teacher is treated as an argument of show_time. In show_time, defined function, wrap , can access the object, func , and can also utilize the function defined in time . Then, add print() into wrap and execute func . Here is the end of the creation of a new function that has additional features and doesn’t change the original function. At the end of show_time , it returned the newly defined function, wrap , to teacher_t .

Next time, when we execute teacher_t , we can not only gain the output of teacher() but also get the start time of the function without changing the code in teacher() . The code in Line 16 does the same things as Line 15.

Another method is to assign the return object to the original declaration. For example, in Line 19, you can assign the return object of show_time to teacher . This way is more intuitive and additional features will automatically be operated when teacher() executed.

Syntax Candy @

If you want to use more than one decorator, you may use the code shown below :

pic 4

In line 18, as you can see, this row of code is too long to identify the order and the name of the decorator. In Python, there is a syntax candy, @, which can help you simplify the usage.

pic 5

Adding the format, @decorator , before the function can easily help you recognize decorator. By means of @, You don’t need to add other codes to the contents. Just use it in the normal way then you can get the enhanced function.

Execution order of decoration

Based on the output, you can see that the order of decorators in the above codes from top to down is show_time first, show_func_name second, teacher last.

It shows that show_func_name is the first one decorating teacher.
First, teacher is passed into show_func_name to be decorated.
Second, the return object of show_func_name, wrap1, is sent to show_time. Finally, the last return object, wrap2 , is assigned to teacher.

Generally, the decorator which is the closest to the decorated function is the first one being executed, then the second closest one, and so on.

That is why the output is showing local time first, showing function name second, and finally, operating the decorated function, teacher . You should pay attention to it when you use it.

Pass arguments into the decorated function

In some situations, the decorated function may need some variables as its arguments. you can add the same number of arguments to the decorating function. Follow codes is an example:

pic 6. The decorating function, wrap( ) , has the same number of arguments as the decorated function, show_info( ).

Sometimes, setting a certain number of arguments for decorating function is limited, because you don’t know what kind of function will be decorated when you are creating a decorator. Maybe the decorated function would have five or more numbers of arguments. In this situation, you can use *args to receive various numbers of positional arguments or use **kwargs to accept keyword arguments based on your argument’s type.

The decorator can decorate two functions that have different numbers of arguments.

Using *args and **kwargs increases the compatibility of the decorator. No more be limited to the number of arguments. The decorator can be used more widely and generally.

If you don’t know the usage shown above, you can check the reference to learn.

Decorator with parameters

This part will show you how to create a decorator with parameters and how it was interpreted in Python. Let’s see an example :

Here I define two functions, one is to be decorated, decorated , and the other one is the decorator, show_func_name. This decorator is to show the name of the decorated function. If I want to pass an argument to decorator, we can not just add another parameter to the definition of show_function_name . We should create another nested function to accept it.

As you can see, I defined a new decorator, decorator_arg( ) to bring the argument in.

Syntax candy, @ , will accept an object id to execute decoration. This object can be a function or class id. When the interpreter reads Line 11, it will first execute the latter part, decorator_arg('red') then operate the decorator.

In the first part, the string, 'red' , will be passed into decorator_arg as parameters. The executed function defined a new function, show_func_name and send it to @ as a return object. @ treats show_func_name as a decorator to decorate the function, decorated .

When @ is decorating, it will automatically let the first argument of the decorator be the object which lies at the line next to @ and operate the decorator.

Finally, it will assign the return object of show_func_name , wrap, to the decorated function, decorated.

Remember that the property of Closure makes the enclosed function can access the local variables of outer functions even though they have ended their operation. After decoration, when you call decorated , it will first access the parameter of decorator_arg, color , and print it at Line 4, "I wish the...” then use the parameter, func, to access its attribute __name__ and print it by Line 5. Finally, execute the decorated function.

This is the whole process of decoration. Hope this introduction is helpful to you.

functools.wraps()

When you use decorators, you may meet the following problem.

At first, Creating the decorator, show_func_name, aims to show the name of the decorated function, "teacher" , but the output is “wrap2”. Why does it caust the result? Based on the operating order of decorator, the decorator being closet to the decorated function will be executed first. show_time will first decorate teacher then its returned value, wrap2, will be decorated by show_func_name . That’s why func.__name__ in wrap1is "wrap2" .

We now know the problem. How can we resolve it? You can use a standard Python module — functools, to conquer it. For example :

Add @functools.wraps(func) before enclosed functions.

As you can see, Adding the decorator, functools.wraps(func) , before the enclosed function solves the problem. The wraps() decorator can make the enclosed function look like the decorated function by copying attributes such as __name__, __doc__ and so on. If you are interested in it, I post the link and you can check it.

--

--