This is the fourth day of my participation in the August More text Challenge. For details, see: August More Text Challenge

Decorators are one of the more difficult things to understand in Python. They are a very common, if not inevitable, question in A Python interview, at least in my experience. Decorators are also frequently used in Python development. Mastering decorators will make your code more concise and your thinking broader. All right, let’s cut to the chase.

What is a decorator

A decorator, in fact, is a decorator function that modifies some of the functions of the original function, so that the original function does not need to be modified. The decorator itself can enhance the function of other functions. If a function is ordinary, it doesn’t matter, but I have my decorator, it will make you shine. So may be a little round, for chestnuts, you are a male programmers, for example, then you wear wigs, wear women’s clothing, and then draw a makeup, then you can be a women’s clothing, wigs, women’s clothing, makeup is a decorator, you are still you, but because of the decorator, you became a female bosses. enmmmm… That’s about it.

Let’s take a look at the simplest function in the World, and relax, it’s definitely not Hello World!

def hello() :
    print("Hello, decorator!)
Copy the code

Ha ha, I am not cheating, do not believe the small partner can privately run ha ha ha.

So if I want the hello() function to do something else, like print the time when the function was executed. Some people say that you can add the print time code to the hello() function.

import datetime


def hello() :
    print("Current time :", datetime.datetime.now())
    print("Hello, decorator!)
Copy the code

If hello1(), hello2(), hello3() and so on are added one by one, it will cause a lot of duplicate code. To avoid duplicate code, we can redefine a function that prints the current time and then executes the real business code after printing the time.

def print_time(func) :
    print("Current time :", datetime.datetime.now())
    func()


def hello() :
    print("Hello, decorator!)


print_time(hello)
Copy the code

That’s not too bad logically, but if we’re passing print_time() a function every time, we’re breaking the structure of our code, where we were calling hello() to execute our business logic, and now we have to call print_time(), Is there a better way? Of course, it’s decorators.

The implementation of decorators

Here’s how the decorator works:

import datetime


def my_decorator(func) :
    def wrapper() :
        print("Current time :", datetime.datetime.now())
        func()
    return wrapper


@my_decorator
def hello() :
    print("Hello, decorator!)
    
    
hello()
Copy the code

Running result:

Current time: 2021-07-31 11:31:46.630720 Hello decorator! Process finished with exit code 0Copy the code

Note: The @ symbol is the syntactic sugar of the decorator and is used when defining a function to avoid another assignment. @my_decorator, in this case, equals

hello = my_decorator(hello) 
hello()
Copy the code

Obviously, not only do you get the functionality you need, but the code is much cleaner. To explain, when the last hello() function is run, the call procedure looks like this:

  • First performhello = my_decorator(hello), the variablehelloPoints to themy_decorator().
  • my_decorator(func)The biography is refshelloAnd the returnedwrapperAnd thewrapperIt calls the original function againhello().
  • So, execute firstwrapper()Function, and then executehello()In the function.

My_decorator () in the above code is a decorator that “enhances” hello() without actually changing the internal implementation of hello().

Decorators with parameters

The wrapper() for the decorator is inserted into the wrapper() and the parameters are inserted into the wrapper(). The wrapper() for the decorator is inserted into the wrapper.

def my_decorator(func) :
    def wrapper(*args, **kwargs) :
        print("Current time :", datetime.datetime.now())
        func(*args, **kwargs)
    return wrapper


@my_decorator
def hello(name) :
    print("Hello {}".format(name))


hello("tigeriaf")
Copy the code

Output:

Current time: 2021-07-31 13:43:59.192468 Hello tigeriaf Process Finished with exit code 0Copy the code

Note: In Python, *args and **kwargs mean to accept any number and type of arguments. See my previous article on how to use *args and **kwargs in Python

Custom parameter

The above decorators all accept external parameters, but the decorators have their own parameters. For example, add a parameter to control the number of times a business function is executed:

def my_decorator_outer(num) :
    def my_decorator(func) :
        def wrapper(*args, **kwargs) :
            print("Current time :", datetime.datetime.now())
            for i in range(num):
                func(*args, **kwargs)
        return wrapper
    return my_decorator


@my_decorator_outer(3)
def hello(name) :
    print("Hello {}".format(name))


hello("tigeriaf")
Copy the code

Note: this is a three-level nested function. The num argument to my_decorator_outer controls the count.

After running, the following information is displayed:

Hello, Tigeriaf hello, Tigeriaf Hello, Tigeriaf Process Finished with exit code 0Copy the code

Built-in decorator @functools.wrap

Now let’s go one step further and print the meta information for the hello() function in the following example:

def my_decorator(func) :
    def wrapper(*args, **kwargs) :
        print("Current time :", datetime.datetime.now())
        func(*args, **kwargs)
    return wrapper


@my_decorator
def hello(name) :
    print("Hello {}".format(name))


# hello("tigeriaf")
print(hello.__name__)  Print meta information of hello function
Copy the code

The result is:

wrapper

Process finished with exit code 0
Copy the code

This means that instead of the hello() function, it has been replaced by a wrapper() function. Function names and other attributes will be changed. Of course, this will not affect the result, but if we need to use metaclass information, how do we retain it? Alas, this can be done with the built-in decorator @functools.wrap.

import functools


def my_decorator(func) :
    @functools.wraps(func)
    def wrapper(*args, **kwargs) :
        print("Current time :", datetime.datetime.now())
        func(*args, **kwargs)
    return wrapper


@my_decorator
def hello(name) :
    print("Hello {}".format(name))


# hello("tigeriaf")
print(hello.__name__)
Copy the code

Let’s run this:

hello

Process finished with exit code 0
Copy the code

Obviously, when writing a decorator, it’s a good idea to add @functools.wrap, which preserves the name and properties of the original function.

Class decorator

The decorator we just touched on was done by a function, and because of Python’s flexibility, we can also use a class to implement a decorator. Classes implement the function of decorators because when we call an instance of the class, its __call__() method is called. Let’s change our previous example to a class decorator:

class MyDecorator:
    def __init__(self, func) :
        self.func = func

    def __call__(self, *args, **kwargs) :
        print("Current time :", datetime.datetime.now())
        return self.func(*args, **kwargs)


@MyDecorator
def hello(name) :
    print("Hello {}".format(name))


hello("tigeriaf")
Copy the code

Running result:

Hello, Tigeriaf Process Finished with exit code 0Copy the code

The order in which layers of decorators are executed

Since decorators enhance functions, what do I want to do if I have multiple decorators? Actually, just add all the decorators you need to use.

@decorator1
@decorator2
@decorator3
def hello() :.Copy the code

But notice that the order of execution is from top to bottom, as shown below:

def my_decorator1(func) :
    @functools.wraps(func)
    def wrapper(*args, **kwargs) :
        print("Decorator 1, current time :", datetime.datetime.now())
        func(*args, **kwargs)
    return wrapper


def my_decorator2(func) :
    @functools.wraps(func)
    def wrapper(*args, **kwargs) :
        print("Decorator 2, current time :", datetime.datetime.now())
        func(*args, **kwargs)
    return wrapper


def my_decorator3(func) :
    @functools.wraps(func)
    def wrapper(*args, **kwargs) :
        print("Decorator 3, current time :", datetime.datetime.now())
        func(*args, **kwargs)
    return wrapper


@my_decorator1
@my_decorator2
@my_decorator2
def hello(name) :
    print("Hello {}".format(name))


hello("tigeriaf")
Copy the code

Running result:

Decorator 1, Current time: 2021-07-31 15:13:23.824342 Decorator 2, Current time: 2021-07-31 15:13:23.824342 2021-07-31 15:13:23.824342 Hello, Tigeriaf Process Finished with exit code 0Copy the code

conclusion

Ok, so much for decorators, but decorators are often used in scenarios where there is a need for aspect, such as performance testing, adding logs, caching, and permission verification. Decorators are a great design solution for this type of problem. With decorators, we can extract a lot of repetitive code that has nothing to do with the functionality of the business function itself, which makes the whole thing simpler. Why not?

Finally, I would like to thank my girlfriend for her tolerance, understanding and support in work and life.