How to understand decorators in Python

First of all, here comes the crap doc engineer again. Start daily hydrological writing. The reason was to look at this question: How do you understand Python decorators? “, just a little while ago, this garbage has embarked on a new round of spicy chicken writing.

Preliminary knowledge

The First thing to understand about decorators is that one of the most important concepts in Python is that a function is a First Class Member. A function is a special type of variable that can be passed to a function as an argument, like any other variable, or returned as a return value.


def abc(a):
    print("abc")

def abc1(func):
    func()

abc1(abc)
Copy the code

The output of this code is the ABC string we output in the function ABC. The procedure is simple. We pass the function ABC as an argument to ABc1, and then call the passed function in ABC1

Let’s look at one more piece of code


def abc1(a):
    def abc(a):
        print("abc")
    return abc
abc1()()

Copy the code

The output of this code is the same as before, here we return the function ABC defined inside ABc1 as a variable, and then we continue to call the returned function after calling abc1 to get the return value.

Ok, let’s do one more thought problem, implement a function add, so that add(m)(n) is the same thing as m+n. If the concept of first-class Member is clear, we can write it clearly

def add(m):
    def temp(n):
        return m+n
    return temp
print(add(1) (2))
Copy the code

Well, the output here is 3.

The body of the

After reading the preliminary knowledge in front, we can begin today’s topic

Let’s start with a requirement

Now we have a function


def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
Copy the code

Now we are going to add some code to this function to calculate the running time of this function.

We thought about it, and we wrote code like this

import time
def range_loop(a,b):
    time_flag=time.time()
    for i in range(a,b):
        temp_result=a+b
    print(time.time()-time_flag)
    return temp_result
Copy the code

Regardless of whether this is accurate or not, we are now going to add a time function to many of the following functions

import time
def range_loop(a,b):
    time_flag=time.time()
    for i in range(a,b):
        temp_result=a+b
    print(time.time()-time_flag)
    return temp_result
def range_loop1(a,b):
    time_flag=time.time()
    for i in range(a,b):
        temp_result=a+b
    print(time.time()-time_flag)
    return temp_result
def range_loop2(a,b):
    time_flag=time.time()
    for i in range(a,b):
        temp_result=a+b
    print(time.time()-time_flag)
    return temp_result
Copy the code

So let’s think for a second, well,Ctrl+ C,Ctrl+V. Emmmm alright, now don’t you think this code is really dirty? What if we want him to get clean?

We thought about it and followed the concept of first-class Member mentioned before. Then I wrote the following code

import time
def time_count(func,a,b):
    time_flag=time.time()
    temp_result=func(a,b)
    print(time.time()-time_flag)
    return temp_result
    
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop1(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop2(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
time_count(range_loop,a,b)
time_count(range_loop1,a,b)
time_count(range_loop2,a,b)
Copy the code

Well, it looks like that, ok, ok, now we have a new problem, we are assuming that all of our functions have only two arguments passed in, so now what if we want to support arbitrary arguments passed in? We scowled and wrote the following code


import time
def time_count(func,*args,**kwargs):
    time_flag=time.time()
    temp_result=func(*args,**kwargs)
    print(time.time()-time_flag)
    return temp_result
    
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop1(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop2(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
time_count(range_loop,a,b)
time_count(range_loop1,a,b)
time_count(range_loop2,a,b)

Copy the code

Ok, now that looks pretty good, this code actually changes the way we call the function. For example, we run range_loop(a,b) directly and still have no way to get the execution time. Now what if we don’t want to change the way the function is called, but we want to get the running time of the function?

Well, that’s easy. I can just substitute


import time
def time_count(func):
    def wrap(*args,**kwargs):
        time_flag=time.time()
        temp_result=func(*args,**kwargs)
        print(time.time()-time_flag)
        return temp_result
    return wrap
    
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop1(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop2(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
range_loop=time_count(range_loop)
range_loop1=time_count(range_loop1)
range_loop2=time_count(range_loop2)
range_loop(1.2)
range_loop1(1.2)
range_loop2(1.2)
Copy the code

Emmmm, how much better does that look? Neither change the original running mode, but also output the running time of the function.

But… Don’t you think manual replacement is disgusting?? Meow meow meow?? Is there anything else I can simplify?

Well, Python knows we’re sugar kids and has provided us with a new syntax sugar, which is also the man of the day, the Decorator

Tell me about the Decorator

We’ve already done this, adding a bit of functionality to the original code without changing the function’s features, but we think it’s disgusting to do this manually. Yes, Python officials think it’s disgusting too, so here comes the syntax candy

Our code above can be written like this


import time
def time_count(func):
    def wrap(*args,**kwargs):
        time_flag=time.time()
        temp_result=func(*args,**kwargs)
        print(time.time()-time_flag)
        return temp_result
    return wrap
@time_count    
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
@time_count
def range_loop1(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
@time_count
def range_loop2(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
range_loop(1.2)
range_loop1(1.2)
range_loop2(1.2)

Copy the code

Wow, writing this, you are not suddenly enlightened! Have a long time to go on my day. Yes, the @ symbol is actually a syntactic sugar that hands off our previous manual substitution to the environment. In human terms, the @ function is passed the wrapped function as a variable to the decorator function/class, replacing the value returned by the decorator function/class with the original function.

@decorator
def abc(a):
    pass
Copy the code

Decorator (ABC) as mentioned earlier, a special substitution occurs ABC =decorator(ABC).


def decorator(func):
    return 1
@decorator
def abc(a):
    pass
abc()
Copy the code

What happens to this code? A: An exception will be thrown. Why ah? A: ABC =decorator(ABC), ABC = 1 because a substitution occurred during decoration. Integers cannot be called as a function by default.


def time_count(func):
    def wrap(*args,**kwargs):
        time_flag=time.time()
        temp_result=func(*args,**kwargs)
        print(time.time()-time_flag)
        return temp_result
    return wrap

def decorator(func):
    def wrap(*args,**kwargs):
        temp_result=func(*args,**kwargs)
        return temp_result
    return wrap

def decorator1(func):
    def wrap(*args,**kwargs):
        temp_result=func(*args,**kwargs)
        return temp_result
    return wrap

@time_count
@decorator
@decorator1    
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
Copy the code

How does this code get replaced? Answer: time_count (decorator (decorator1 (range_loop)))

Well, now you have a basic understanding of what decorators are all about?

Extend the

Now, I would like to modify the time_count function to support passing a flag argument. If flag is True, it outputs the time of the function, and if flag is False, it does not output the time

Let’s start by assuming that the new function is called time_count_plus

Here’s what we want to achieve

@time_count_plus(flag=True)
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
Copy the code

We call time_count_plus(flag=True) once and replace range_loop with its return value as a decorator function, OK so time_count_plus takes an argument and returns a function, right

def time_count_plus(flag=True):
    def wrap1(func):
        pass
    return wrap1
Copy the code

Okay, so now we’re returning a function as a decorator function, and we’re saying that @ actually triggers a replacement, okay so what we’re doing is we’re going to replace range_loop=time_count_plus(flag=True)(range_loop) okay, now you should be clear, Should we also have a function in WRAP1 that returns?

Well, the final code looks like this

def time_count_plus(flag=True):
    def wrap1(func):
        def wrap2(*args,**kwargs):
            if flag:
                time_flag=time.time()
                temp_result=func(*args,**kwargs)
                print(time.time()-time_flag)
            else:
                temp_result=func(*args,**kwargs)
            return temp_result
        return wrap2
    return wrap1
@time_count_plus(flag=True)
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result

Copy the code

Is not so clear much!

Extended two times

Well, now we have a new requirement

m=3
n=2
def add(a,b):
    return a+b

def sub(a,b):
    return a-b

def mul(a,b):
    return a*b

def div(a,b):
    return a/b

Copy the code

Now we have the string a, and the value of a may be +, -, *, /. Now what if we want to call the corresponding function based on the value of a?

Let’s make an egg and think, well, logically


m=3
n=2
def add(a,b):
    return a+b

def sub(a,b):
    return a-b

def mul(a,b):
    return a*b

def div(a,b):
    return a/b
a=input('Please enter either of the + - * / \n')
if a=='+':
    print(add(m,n))
elif a==The '-':
    print(sub(m-n))
elif a==The '*':
    print(mul(m,n))
elif a=='/':
    print(div(m,n))
Copy the code

But isn’t there too much if else in this code? Let’s think about it, using the first-class Member feature, and then working with dict to associate operators with functions.

m=3
n=2
def add(a,b):
    return a+b

def sub(a,b):
    return a-b

def mul(a,b):
    return a*b

def div(a,b):
    return a/b
func_dict={"+":add,"-":sub,"*":mul,"/":div}
a=input('Please enter either of the + - * / \n')
func_dict[a](m,n)
Copy the code

Emmmm, looks great, but can we simplify the registration process a little bit? Well, that’s where the decorator syntax feature comes in

m=3
n=2
func_dict={}
def register(operator):
    def wrap(func):
        func_dict[operator]=func
        return func
    return wrap
@register(operator="+")
def add(a,b):
    return a+b
@register(operator="-")
def sub(a,b):
    return a-b
@register(operator="*")
def mul(a,b):
    return a*b
@register(operator="/")
def div(a,b):
    return a/b

a=input('Please enter either of the + - * / \n')
func_dict[a](m,n)
Copy the code

Well, remember when we said that using the @ syntax actually triggers a substitution? Here we take advantage of this feature by registering the function map when the decorator fires so that we get the function processing data directly based on the value of ‘a’. Also notice that we don’t have to change the original function here, so we don’t have to write the third level function.

For Flask, you will know that this feature is used when the route method is called to register a route. For Flask, you will find that the router is the same as the router

conclusion

In fact, the whole article, you should know a little bit about this. Decorators in Python are a further application of the first-class Member concept, where we pass functions to other functions, wrap new functions and return. @ is just a simplification of the process. Decorators are ubiquitous in Python, and many of the official library implementations rely on them, such as a garbage hydrologic Python descriptor primer written a long time ago.

Well, that’s all for today!