Manjusaka

Manjusaka

How to understand decorators in Python

How to Understand Decorators in Python#

First of all, this garbage document engineer is back. Starting the daily writing of water articles. The reason is that I saw this question How to Understand Python Decorators?, and I happened to explain this to someone not long ago, so this garbage started another round of writing trash articles.

Prerequisite Knowledge#

To understand decorators, the first important concept to grasp in Python is: "Functions are First Class Members." To translate this further, functions are a special type of variable that can be passed as parameters to other functions and can also be returned as return values.


def abc():
    print("abc")

def abc1(func):
    func()

abc1(abc)

The output of this code is the string abc that we output in the function abc. The process is simple; we pass the function abc as a parameter to abc1, and then call the passed function in abc1.

Now let's look at another piece of code.


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

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

Now, let's think about a problem: implement a function add such that add(m)(n) is equivalent to m+n. If we clarify the previous concept of First-Class Member, we can write it out clearly.

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

Well, the output here is 3.

Main Text#

After looking at the prerequisite knowledge, we can start today's topic.

Let's look at a requirement first.#

Now we have a function.


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

Now we want to add some code to this function to calculate its running time.

We might think of writing 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

Putting aside whether this method of calculating time is accurate, we now want to add a time calculation feature to many functions 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
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

We might think, hmm, Ctrl+C, Ctrl+V. Emmmm, now don't you think this code is particularly messy? How can we make it cleaner?

We thought about it and, according to the previously mentioned concept of First-Class Member, 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)

Hmm, it looks somewhat reasonable. Alright, now we have a new problem. We are assuming that all our functions only take two parameters. Now, what if we want to support passing any number of parameters? We frowned 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)

Now it looks a bit more like it, but let's think again. This code actually changes our function calling method. For example, if we directly run range_loop(a,b), we still cannot get the function execution time. So now, if we don't want to change the function calling method and still want to get the running time, what should we do?

It's simple, just replace it.


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)

Emmmm, now it feels much more comfortable, right? It neither changes the original running method nor outputs the function running time.

But... don't you think manually replacing is too disgusting??? Meow meow meow??? Is there anything that can simplify this?

Alright, Python knows we are kids who love candy, and it provides us with a new syntax sugar, which is today's main character, Decorators.

Let's Talk About Decorators#

We have already implemented adding new functionality to existing code without changing the function characteristics, but we also feel that this manual replacement is too disgusting. Yes, the Python official also thinks this is very disgusting, so the new syntax sugar has arrived.

The code above can now 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)

Wow, writing to this point, do you suddenly realize something! まさか??? Yes, the @ symbol is actually a syntax sugar that hands over the manual replacement process to the environment. To put it in plain language, the role of @ is to pass the wrapped function as a variable to the decorator function/class and replace the original function with the value returned by the decorator function/class.

@decorator
def abc():
    pass

As mentioned earlier, a special replacement process actually occurs: abc=decorator(abc). Now let's do a few exercises to practice.


def decorator(func):
    return 1
@decorator
def abc():
    pass
abc()

What will happen in this code? Answer: An exception will be thrown. Why? Answer: Because during decoration, a replacement occurs: abc=decorator(abc), and after replacement, the value of abc is 1. An integer 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

How does this code replace? Answer: time_count(decorator(decorator1(range_loop))).

Hmm, now do you have a basic understanding of decorators?

Let's Expand a Bit#

Now, I want to modify the previously written time_count function to support passing a flag parameter. When flag is True, it outputs the function running time; when False, it does not output the time.

Let's take it step by step. We first assume the new function is called time_count_plus.

We want the effect to be like this.

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

Hmm, looking at this, first we call time_count_plus(flag=True) once, and use the value it returns as a decorator function to replace range_loop. OK, so time_count_plus needs to accept a parameter and return a function, right?

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

Alright, now we have returned a function to serve as the decorator function. Then we said that @ actually triggers a replacement process. So now our replacement is range_loop=time_count_plus(flag=True)(range_loop). Now everyone should be very clear that we should also have a function inside wrap1 and return it.

Hmm, 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

Isn't it much clearer this way!

A Little More Expansion#

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

Now we have a string a, and the value of a could be +, -, *, or /. Now, we want to call the corresponding function based on the value of a.

We thought about it and, hmm, logical judgment.


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 any one of + - * /\n')
if a=='+':
    print(add(m,n))
elif a=='-':
    print(sub(m,n))
elif a=='*':
    print(mul(m,n))
elif a=='/':
    print(div(m,n))

But isn't this code too much with if else? We thought about it and used the First-Class Member feature along with a dict to establish a connection between operators and 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 any one of + - * /\n')
func_dict[a](m,n)

Emmmm, it looks good, but can we simplify the registration process? Hmm, at this point, the decorator syntax feature can be used.

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 any one of + - * /\n')
func_dict[a](m,n)

Hmm, remember when we said that using the @ syntax actually triggers a replacement process? Here we utilize this feature to register function mappings when the decorator is triggered, allowing us to directly obtain the function to process data based on the value of 'a'. Also, please note that we do not need to modify the original function, so we do not need to write a third-level function.

If you are familiar with Flask, you will know that when calling the route method to register routes, this feature is also used. You can refer to another article I wrote a long time ago Beginner's Reading of Flask Source Code Series (1): An Initial Exploration of Flask's Router.

Conclusion#

Throughout this article, you should have learned something. Decorators in Python are actually a further application of the First-Class Member concept, where we pass functions to other functions, wrap them with new functionality, and then return them. The @ symbol simply simplifies this process. In Python, decorators are ubiquitous, and many implementations in official libraries also rely on decorators, such as an article I wrote a long time ago Beginner's Reading of Flask Source Code Series (1): An Initial Exploration of Flask's Router.

Well, let's stop here for today!

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.