高阶函数之装饰器(1)

当我们实现了某个核心模块(函数)后,可能按照实际情况需要为这个模块添加额外的功能,例如统计程序运行时间、记录日志等等。此时,我们并不希望去修改模块本身的代码(想想都觉得很low),也不希望改变函数的调用方式(不然那么多调用每个都去改岂不是也很low)。装饰器 提供了解决这一问题的方案。简单来说,装饰器可以在核心代码的外面套一层壳,执行这段带壳的代码就能达到我们的目的。我将从一个 Python 渣渣的角度理解什么是 装饰器

套壳这一操作通过 @语法糖 实现,@语法糖指定了使用什么壳(装饰器函数的引用),壳的内容通过 装饰器函数 定义。

我们在面向对象编程时用到的 @staticmethod 和 @classmethod 就属于装饰器的一种。

这里只以最简单的 ”壳“ 理解装饰器的执行逻辑。

最基础的装饰器

装饰器会对目标函数进行包装,然后将目标函数名指向包装后的函数定义。一般语法是

1
2
@decorator
def func(*args, **kwargs): pass

这里我们将 decorator 称之为 装饰器函数,将 func 称之为 目标函数。这里我们需要注意的是,使用 @ 语法时不仅仅是在定义目标函数,也是在调用装饰器函数,搞清楚这一点就能理解 @ 语法的作用机制。

一般来说,装饰器函数的 引用 紧跟在 @ 符号后面,紧接着下一行就是目标函数的定义。装饰器函数只接受一个参数,那就是目标函数。所以说,装饰器函数是一个 高阶函数,它以函数引用作为参数,同时返回另一个函数的引用。

当程序检测到 @ 符号时,会 调用(或者叫 执行) 装饰器函数,传入目标函数的引用,返回另外一个函数的引用,并将指向原来目标函数的变量(上面的 func 变量)指向返回的新函数。所以上面的示例代码就相当于:

1
2
3
4
5
## 定义一个函数(具体实现略),并定义一个变量func(函数变量)指向这个函数
def func(*args, **kwargs): pass

## 调用装饰器函数,并使func变量指向装饰器返回的那个函数
func = decorator(func)

说完了 @ 符号,我们再来谈谈 装饰器函数 本身。上面提到了,装饰器函数接受目标函数作为参数并返回一个新的函数。

我们之所以称之为 装饰器,是因为我们想扩展目标函数的功能,例如统计目标函数的执行时间等等。

换言之,目标函数定义了模块的核心功能,而某些时候我们需要对该模块做一些拓展,例如统计运行时间。

所以装饰器函数的基本形式应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
def decorator(func):
## 我们加上下面这一句可以验证执行到@时是否执行了装饰器函数
print('run decorator ...')
def wrapper(*args, **kwargs):
## do something, such as
print('run wrapper ...')
result = func(*args, **kwargs)
## do something, such as
print('wrapper end!')
return result
print(wrapper)
return wrapper

上面描述了一个装饰器函数的基本模型,它主要定义了两个东西:

  • 局部的函数变量 func:decorator.<locals>.func,它指向了目标函数
  • 内嵌的函数 wrapper:decorator.<locals>.wrapper,目标函数的函数变量将指向它

执行 decorator 函数返回了另外一个函数 wrapper 的引用,显然这个函数的 __name__ 属性是 wrapper。

下面是用装饰器修饰目标函数的一个例子:

1
2
3
@decorator
def exp(x, y):
return x ** y

运行这段代码的输出是:

1
2
run decorator ...
<function decorator.<locals>.wrapper at 0x0000023AEDADC488>

可以看到,@ 确实执行了装饰器函数 decorator,同时 定义 了一个函数 wrapper。

但是 wrapper 函数并没有执行,我们再次

1
2
3
4
print(exp(2,3))
#> run wrapper ...
#> wrapper end!
#> 8

可以看到,此时执行了 wrapper 函数,同时返回了计算结果,最终使用 print 函数打印了返回结果。

检查函数变量 exp 的 __name__ 属性,发现不再是 “exp”,而是 “wrapper”:

1
2
exp.__name__
#> 'wrapper'

带参数的装饰器

此外,还有一种带参数的装饰器,例如:

1
2
3
4
5
@auth('barwe')
def access_database(*args, **kwargs):
print("访问某个数据库...")

access_database()

假定的执行结果是

1
2
3
函数(access_database)准备授权给用户(barwe)...
访问某个数据库...
授权完成!

我们回忆一下上面提到的规则:@ 后面应紧跟 装饰器函数 的引用。这里很明显 auth('barwe') 是调用了 auth 函数,而且它的参数是一个字符串。那么可以预见的是,auth('barwe') 应该返回一个真正的装饰器函数的引用,然后再解释 @ 符号,再调用一次装饰器函数。另外,从打印结果我们知道,装饰器函数还可以访问 auth 函数的局部变量的值 “barwe”。不难定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def auth(name):
#print('start auth ...')
def _auth(func):
#print('start _auth ...')
def wrapper(*args, **kwargs):
#print('start wrapper ...')
print(f'函数({func.__name__})准备授权给用户({name})...')
result = func(*args, **kwargs)
print('授权完成!')
#print('wrapper end!')
return result
#print('_auth end!')
return wrapper
#print('auth end!')
return _auth

@auth('barwe')
def access_database(*args, **kwargs):
print("访问某个数据库...")

如果去掉上面 print 函数的注释再次执行,就能得到

1
2
3
4
start auth ...
auth end!
start _auth ...
_auth end!

这表示先调用了 auth('barwe') ,再解释 @ 时又调用了 auth.<locals>._auth(access_database)

但是 access_database <- auth.<locals>._auth.<locals>.wrapper 并没有执行!

我们正式执行修饰后的目标函数:

1
access_database()

输出结果是

1
2
3
4
5
start wrapper ...
函数(access_database)准备授权给用户(barwe)...
访问某个数据库...
授权完成!
wrapper end!

总结一下,@ 符号会调用一次装饰器函数。如果是带参的装饰器,先执行后面的函数,然后再解释 @,这就要求后面那个函数应该返回一个标准的装饰器函数(即以目标函数为参数,返回对目标函数进行修饰后的新函数)。

----- For reprint please indicate the source -----
0%