One skill a day: the execution order of Python decorators

Original link: https://www.kingname.info/2023/04/16/order-of-decorator/

When it comes to the execution order of Python decorators, there are a lot of half-baked ones:

The decorators near the function name are executed first, and the decorators far away from the function name are executed after.

This statement is not accurate. But most of these half-assed people will still be dissatisfied. They will throw out a piece of code to you to “prove” their point of view:

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 def decorator_outer ( func ):
print ( "I am the outer decorator" )
def wrapper ():
func()
return wrapper

def decorator_inner ( func ):
print ( "I am the inner decorator" )
def wrapper ():
func()
return wrapper

@decorator_outer
@decorator_inner
def func ():
print ( "I am the function itself" )

func()

The running effect is shown in the figure below:

20230415230554.png

The decorator_inner is close to the function name, it is an inner decorator, and print inside it is printed out first; decorator_outer is far away from the function name, it is an outer decorator, and print inside it is printed out later. It seems that内层装饰器先执行,外层装饰器后执行.

Why do I say this view is inaccurate? Let’s take a look at the following code:

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
25
26
27
28
29
30
31
32
 def decorator_outer ( func ):
print ( "I am the outer decorator" )
print ( 'a' )
print ( 'b' )
def wrapper ():
print ( 'outer decorator, before the function runs' )
func()
print ( 'outer decorator, after the function runs' )
print ( 'The outer decorator closure has been initialized' )
print ( 'c' )
print ( 'd' )
return wrapper

def decorator_inner ( func ):
print ( "I am the inner decorator" )
print ( 1 )
print ( 2 )
def wrapper ():
print ( 'Inner decorator, before the function runs' )
func()
print ( 'Inner decorator, after the function runs' )
print ( 'The inner decorator closure is initialized' )
print ( 3 )
print ( 4 )
return wrapper

@decorator_outer
@decorator_inner
def func ():
print ( "I am the function itself" )

func()

The running effect of the above code is shown in the following figure:

20230415232718.png

As can be seen from the figure, in the code inside the decorator, the code outside wrapper closure is indeed executed first by the inner decorator and after the outer decorator. But the code inside the closure wrapper is a little more complicated:

  1. The outer decorator is executed first, but only part of it is executed until func() is called
  2. The inner decorator starts executing
  3. The inner decorator is executed
  4. The outer decorator is executed

The execution effect is somewhat similar to:

 1
2
3
4
5
6
7
8
9
10
11
12
 def func ():
print ( 'I am the function itself' )

def deco_inner ():
print ( 'Inner decorator, before the function runs' )
func()
print ( 'Inner decorator, after the function runs' )

def deco_outer ():
print ( 'outer decorator, before the function runs' )
deco_inner()
print ( 'outer decorator, after the function runs' )

The running effect is shown in the figure below, which is consistent with the running order of each wrapper closure in the decorator.

20230415233918.png

Therefore, when we say that when multiple decorators are stacked, which decorator’s code runs first, we cannot say that the code of the inner decorator runs first. This will give people the illusion that the code of the inner decorator is run first from the first line to the last line. The accurate statement should be that the code outside wrapper is indeed run first by the inner decorator, and then by the outer decorator. But the code inside wrapper is that the outer decorator先开始运行,后运行完毕, and the inner decorator后开始运行,先运行完毕.

This knowledge seems a bit like interview stereotypes, what’s the use? Let me give you an example. The following is an interface written using FastAPI:

 1
2
3
4
5
6
7
8
9
10
11
12
13
 from fastapi import FastAPI
app = FastAPI()

def do_query_dataset ( dataset_id ):
print ( "Read the database directly to get dataset information" )
dataset_info = { "xxx" : 1 , "yyy" : 2 }
return dataset_info


@app.get( '/dataset' )
def get_dataset ( dataset_id: int ):
dataset_info = do_query_dataset(dataset_id)
return { 'success' : True , "data" : dataset_info}

The user accesses this interface, and the parameter dataset_id is passed in the URL to obtain the information of the dataset. As shown below:

20230416100904.png

Now, to add permission verification, first determine whether the user is logged in or not. In the case that the user has logged in, check whether the user has the permission of this data set. Data set information can only be returned when there is permission for this data set.

You must have thought of using a decorator to do these two steps. The code you wrote at the beginning might look like this:

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
 def check_login ( func ):
def wrapper ( *args, **kwargs ):
print ( 'Check if there are specific Cookies' )
is_login = False
if not is_login:
return { 'success' : False , "msg" : "Not logged in" }
return func(*args, **kwargs)
return wrapper


def check_data_set_permission ( func ):
def wrapper ( *args, **kwargs ):
print ( 'Check if there is a specific dataset permission' )
print ( 'First get dataset_id from the request parameter' )
print ( 'Then get the user id from the login session, note that if there is no login, there is no session' )
print ( 'Judge whether the user has the permission of this dataset' )
has_data_set_permission = True
if not has_data_set_permission:
return { 'success' : False , "msg" : "No dataset permission" }
return func(*args, **kwargs)
return wrapper

At this time, we need to ensure that the code in check_login to check whether the user is logged in runs first. Then it can be the code to check the permission of the data set in check_data_set_permission .

The half-bad guy at the beginning of this article thinks that the decorator close to the function name is executed first, and the decorator far away from the function name is executed after. According to their theory, it would be written as:

 1
2
3
4
 @check_data_set_permission
@check_login
def do_query_dataset ( dataset_id ):
...

It is obviously wrong to write this way. Because check_data_set_permission decorator has a premise that the user has logged in, the code will come here. Then he will go directly to the session to get the user ID. A user who is not logged in does not have a user ID. There will be an error in this step of taking the ID.

According to the explanation above in this article, since these two logics are inside wrapper .
For the code inside wrapper , the outer decorator starts running first. Therefore, the correct order of our decorators here can only be arranged in the following order:

 1
2
3
4
 @check_login
@check_data_set_permission
def do_query_dataset ( dataset_id ):
...

This way of writing, intuitively, will contradict the cognition at the beginning of this article. But that’s the correct order.

This article is transferred from: https://www.kingname.info/2023/04/16/order-of-decorator/
This site is only for collection, and the copyright belongs to the original author.