Python101: 20. Decorators

post thumb
Python
by Admin/ on 18 Jul 2021

Python101: 20. Decorators


1. Introduction to the concept


decorator, also known as a “decorator function”, is a function that returns a value that is also a function, and can be called a “function of functions”. Its purpose is to implement additional functionality without modifying existing functions. The most basic idea comes from a design pattern called the “decoration pattern”.

In Python, decorators are pure “syntactic sugar”, and it’s okay not to use them, but they can greatly simplify code and make it more readable - for those who know what’s going on, of course.

You’ve probably seen the @ symbol in Python code after studying it for a while. Yes, this symbol is the marker for using decorators, and it’s proper Python syntax.

Syntactic sugar: A syntax added to a computer language that has no effect on the functionality of the language, but is more convenient for the programmer to use. Generally speaking the use of syntactic sugar can increase the readability of a program, thus reducing the chance of errors in the program code.

2. operation mechanism


In short, the following two pieces of code are semantically equivalent (although there is a slight difference in the exact process).

def IAmDecorator(foo):
    '''I'm a decorator function'''
    pass

@IAmDecorator
def tobeDecorated():
    '''I am the decorated function'''
    pass

With.

def IAmDecorator(foo):
    '''I'm a decorator function'''
    pass

def tobeDecorated() :
    '''I am the decorated function'''
    pass
tobeDecorated = IAmDecorator(tobeDecorated)

As you can see, using the @ syntax of the decorator is equivalent to passing the concrete defined function as an argument to the decorator function, which in turn goes through a series of operations to return a new function, and then assigning this new function to the original function name.

What we end up with is a new function that is same name as the function we explicitly defined in the code and heterogeneous.

The decorated function is like a shell for the original function. As shown in the figure, the resulting combined function is the new function generated by applying the decorator.

It is important to note that there is a slight difference in the execution of the above two pieces of code. In the second code, the function name tobeDecorated actually points to the original function first, and only after the decorator modification, it points to the new function; but in the first code, there is no such intermediate process, and the new function named tobeDecorated is obtained directly.

In addition, the decorated function **has and can have only one argument, the original function to be decorated.

3. Usage


In Python, there are two types of decorators, “function decorators” and “class decorators”, with “function decorators” being the most common and “class decorators” being the least used. “class decorators” are rarely used.

3.1 Function decorators

3.1.1 General structure

The definition of a decorative function can be roughly summarized in the template shown below, i.e.

Illustration
Decorating func
Internal func
Return statement

Since the return value of the decorated function is also required to be a function, in order to expand the function on top of the original function and make the expanded function return as a function, it is necessary to define an internal function in the definition of the decorated function and further manipulate it in this internal function. The final return object should be this internal function object, and only then can a function with new functions be returned correctly.

The decorator function is like a “wrapper” that fits the original function inside the decorator function, thus extending the original function by adding functions to it, and the decorator function returns the new whole. At the same time, the original function itself will not be affected. This is also the meaning of the word “decorate”.

Would it be okay if we didn’t define “internal functions” in this place?

The answer is “no”.

3.1.2 Explanation of the structure

Let’s take a look at the following code.

def IAmFakeDecorator(fun):
    print("I'm a fake decorator")
    return fun

@IAmFakeDecorator
def func():
    print("I am the original function")

# I'm a fake decorator

It’s a bit strange how the operation of the decorator extension is executed just as soon as it is defined.

To call the new function again.

func()
# I am the original function

Eeyo strange, where is the extended function ah?

Don’t be anxious, let’s analyze the above code. In the definition of the decorated function, we do not define a separate internal function, the extended operation is directly placed in the function body of the decorated function, and the return value is the original function passed in.

When defining a new function, the following two pieces of code are again equivalent.

@IAmFakeDecorator
def func():
    print("I am the original function")

# I'm a fake decorator

and

def func():
    print("I am the original function")

func = IAmFakeDecorator(func)
# I am a false decorator

Looking at the latter code, we can see that the decorator is only called once while defining the new function, after that the object referenced by the new function name is the return value of the decorator, which has nothing to do with the decorator.

In other words, the operations in the function body of the decorator itself are **executed once when and only when **the function is defined, and when the function is called later with the new function name, the operations performed will only be those of the internal function. So by the time the new function is actually called, the result obtained is no different from the original function.

Simply returning the incoming original function without defining an inner function is certainly possible and meets the requirements of a decorator; however, it does not get the result we expect, and the functionality extended to the original function is not reusable and is only one-off. Therefore such behavior does not make any sense.

The function defined inside the decorated function for extending the function can be named as you like, but the general convention is to name it wrapper, which means wrapping.

The correct definition of a decorator should look like the following.

def IAmDecorator(fun):
    def wrapper(*args, **kw):
        print("I'm really a decorator")
        return fun(*args, **kw)
    return wrapper

3.1.3 Problems with parameter settings

The purpose of setting internal function parameters to (*args, **kw) is to be able to receive arbitrary arguments. The content of how to receive arbitrary arguments is described in the previous function parameters section has been described.

The reason why we want wrapper to be able to take arbitrary arguments is that when we define the decorator we don’t know what function it will be used to decorate and what the arguments of the specific function will be; defining it as “can take arbitrary arguments” can greatly enhance the adaptability of the code.

Also, note the location of the given parameters.

To clarify the concept: once the function parameters are given elsewhere than in the function header, the meaning of the expression is no longer “a function object”, but “one function call”.

Therefore, the purpose of our decorator is to return a function object, the object of the return statement must be the name of the function without arguments; in the internal function, we are required to call the original function, so we need to bring function parameters, otherwise, if the return value of the internal function is still a function object, you still need to give another set of parameters to be able to call the original function. show code.

def IAmDecorator(fun):
    def wrapper(*args, **kw):
        print("I'm really a decorator")
        return fun
    return wrapper

@IAmDecorator
def func(h):
    print("I am the original function")

func()
# I'm really a decorator
# <function func at 0x000001FF32E66950>

The original function is not called successfully, but only the function object corresponding to the original function is obtained. The correct call can occur only if the next set of parameters is given further (to demonstrate the effect of the parameters, an additional parameter h is added to the definition of the function func).

func()(h=1)
# I'm really a decorator
# I am the original function

As long as you understand the difference between with and without arguments, and know exactly what you want, you won’t make mistakes with arguments. And there is no need to stick to the above rules at all, maybe you want an uncalled function object?

With this in mind, nested decorators and nested inner functions are no longer a problem.

3.1.4 Function Properties

It should also be noted that after the decorator modification, the properties of the original function are also changed.

def func():
    print("I am the original function")

func.__name__
# 'func'

Normally, to define a function, its function name and the corresponding variable should be the same, so that unnecessary problems can be avoided when some need to identify and index the function object by the variable name. But things do not go so smoothly.

@IAmDecorator
def func():
    print("I am the original function")

func.__name__
# 'wrapper'

The variable name is still the same, the original function is still the same, but the function name becomes the name of the internal function in the decorator.

Here we can use the wraps tool in Python’s built-in module functools for the purpose of “extending functions with decorators while preserving the properties of the original function”. Here functools.wraps is itself a decorator. The result is as follows.

import functools
# Define decorators that preserve the properties of the original function
def IAmDecorator(fun):
    @functools.wraps(fun)
    def wrapper(*args, **kw):
        print("I'm really a decorator")
        return fun(*args, **kw)
    return wrapper

@IAmDecorator
def func():
    print("I am the original function")

func.__name__
# 'func'

Great job!

3.2 Class decorators

The concept of a class decorator is similar to that of a function decorator, and the syntax is similar in its use.

@ClassDecorator
class Foo:
    pass

Equivalent to

class Foo:
    pass
Foo = ClassDecorator(Foo)

When defining a class decorator, ensure that both __init__ and __call__ methods exist in the class. The __init__ method is used to receive the original function or class, and the __call__ method is used to implement the decoration logic.

In short, the __init__ method is responsible for binding the incoming function or class to the class instance when it is initialized, while the __call__ method is pretty much the same as a normal function decorator, even the construction is not much different, so you can think of the __call__ method as a function decorator, so I won’t go into it again.

3.3 The case of multiple decorators

Multiple decorators can be nested, and the specifics can be understood as a composite function combined from the bottom up; or it can also be understood as the value of the next decorator is the argument of the previous decorator.

As an example, the following two pieces of code are equivalent.

@f1(arg)
@f2
def func():
    pass

and

def func():
    pass
func = f1(arg)(f2(func))

This situation is also easy to grasp once you understand the previous sections.

4. Summary


This article introduces the decorator feature in Python, explaining in detail how it works and how to use it, which can greatly help learners master the knowledge of decorators, reduce the resistance to reading Python code, and write more pythonic code.


Reference

[1] Python3 Glossary - Decorators

[2] Python3 Documentation - Compound Statements - Function Definitions

[3] Python3 Documentation - Compound Statements - Class Definitions

[4] Syntactic Sugar

[5] Xuefeng Liao’s official website-Python-Tutorial-Functional-Programming-Decorator

[6] Python-100-days-day022

comments powered by Disqus