说到Python装饰器的执行顺序,有很多半吊子张口就来:
靠近函数名的装饰器先执行,远离函数名的装饰器后执行。
这种说法是不准确的。
但是这些半吊子多半还会不服,他们会甩出一段代码给你,来『证明』自己的观点:
def decorator_outer(func):
print("我是外层装饰器")
def wrapper():
func()
return wrapper
def decorator_inner(func):
print("我是内层装饰器")
def wrapper():
func()
return wrapper
@decorator_outer
@decorator_inner
def func():
print("我是函数本身")
func()
运行效果如下图所示:
decorator_inner
这个装饰器靠近函数名,是内层装饰器,他里面的print
先打印出来;decorator_outer
远离函数名,是外层装饰器,它里面的print
后打印出来。看起来确实是内层装饰器先执行,外层装饰器后执行
。
为什么我说这种看法是不准确呢?我们来看看下面这段代码:
def decorator_outer(func):
print("我是外层装饰器")
print('a')
print('b')
def wrapper():
print('外层装饰器,函数运行之前')
func()
print('外层装饰器,函数运行之后')
print('外层装饰器闭包初始化完毕')
print('c')
print('d')
return wrapper
def decorator_inner(func):
print("我是内层装饰器")
print(1)
print(2)
def wrapper():
print('内层装饰器,函数运行之前')
func()
print('内层装饰器,函数运行之后')
print('内层装饰器闭包初始化完毕')
print(3)
print(4)
return wrapper
@decorator_outer
@decorator_inner
def func():
print("我是函数本身")
func()
上面这个代码的运行效果如下图所示:
从图中可以看到,装饰器里面的代码中,wrapper
闭包外面的代码确实是内层装饰器先执行,外层装饰器后执行。但是在闭包wrapper
内部的代码,却稍微复杂一些:
- 外层装饰器先执行,但只执行了一部分,执行到调用
func()
- 内层装饰器开始执行
- 内层装饰器执行完
- 外层装饰器执行完
这个执行效果有点类似于:
def func():
print('我是函数本身')
def deco_inner():
print('内层装饰器,函数运行之前')
func()
print('内层装饰器,函数运行之后')
def deco_outer():
print('外层装饰器,函数运行之前')
deco_inner()
print('外层装饰器,函数运行之后')
运行效果如下图所示,跟装饰器里面各个wrapper
闭包的运行顺序是一致的。
所以,当我们说多个装饰器堆叠的时候,哪个装饰器的代码先运行时,不能一概而论说内层装饰器的代码先运行。这会给人一种错觉,认为是内层装饰器的代码从第一行到最后一行都是先运行的。准确的说法应该是,wrapper
外面的代码,确实是内层装饰器先运行,外层装饰器后运行。但是wrapper
里面的代码,是外层装饰器先开始运行,后运行完毕
,内层装饰器后开始运行,先运行完毕
。
这个知识看起来似乎有点像面试八股文,有什么用呢?我给大家举个例子。下面是使用FastAPI写的一个接口:
from fastapi import FastAPI
app = FastAPI()
def do_query_dataset(dataset_id):
print("直接读取数据库,获取dataset信息")
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}
用户访问这个接口,URL中传入参数dataset_id
,就可以获得数据集的信息。如下图所示:
现在,要增加权限校验,首先要判断用户是否登录。在用户已经登录的情况下,看这个用户是否有这个数据集的权限。在有这个数据集的权限时,才能返回数据集信息。
你肯定想到了使用装饰器来做这两步,一开始你写的代码可能是这样的:
def check_login(func):
def wrapper(*args, **kwargs):
print('检测是否有特定的Cookies')
is_login = False
if not is_login:
return {'success': False, "msg": "没有登录"}
return func(*args, **kwargs)
return wrapper
def check_data_set_permission(func):
def wrapper(*args, **kwargs):
print('检测是否有特定的数据集权限')
print('首先从请求参数中获取dataset_id')
print('然后从登录session中获取用户id,注意,如果没有登录,是没有session的')
print('判断用户是否有这个dataset的权限')
has_data_set_permission = True
if not has_data_set_permission:
return {'success': False, "msg": "没有数据集权限"}
return func(*args, **kwargs)
return wrapper
这个时候,我们要确保check_login
里面检查用户是否登录的代码首先运行。然后才能是check_data_set_permission
里面检查数据集权限的代码。
本文开头的半吊子,认为靠近函数名的装饰器先执行,远离函数名的装饰器后执行。按他们理论,就会写成:
@check_data_set_permission
@check_login
def do_query_dataset(dataset_id):
...
这样写显然是错误的。因为check_data_set_permission
装饰器会有一个前提,就是用户已经登录了,代码才会走到这里。那么他就会直接去session取用户ID。没有登录的用户是没有用户ID的。在取ID的这一步就会出错。
根据本文上面的解释,由于这两个逻辑都是在wrapper
内部的。wrapper
内部的代码,外层装饰器先开始运行。因此,这里我们装饰器的正确顺序,只能按照如下顺序排列:
@check_login
@check_data_set_permission
def do_query_dataset(dataset_id):
...
这个写法,从直觉上,就会跟本文开头的认知矛盾。但这才是正确的顺序。