函数装饰器
这篇文章我们来讲 decorator,也就是装饰器。很多刚学 Python 的人会对这个概念感到非常迷茫,因为很多其他的语言并没有这样一个语法。然后你去网上找例子跟教程,它的例子往往也让人望而却步。就像这样,哪怕最简单的例子看起来都很复杂。那今天我就把 decorator 这件事情给掰开了、揉碎了,好好地讲明白它到底是什么。以后你再看到它,就再也不会迷茫了。
函数对象
那讲 decorator 之前,我们要先理清楚 Python 的一些基础知识。第一件事就是:函数是什么?那可能有些人知道,在 Python 中,所有东西都是对象,都是 object。函数也不例外,函数只是 Python 中的一个普通的 object 而已。
那我们来看一下下面这几行代码编译之后的字节码。
import dis
s = """
def double(x):
return x * 2
print(double(2))
"""
dis.dis(s)
它没有什么神秘的,函数体的部分被编译成了一个 code object,这里面保存这个代码运行的逻辑。然后我们看上面这个部分,它就是把这个 code object load 了进来,然后做了一个 function object。MAKE_FUNCTION根据代码对象创建函数对象;创建函数时不会执行函数体。 就是把这个 code object 变成一个有名字的 function object,然后把这个 function object 保存到了 double 这个名字里。
所以说,当我们在 Python 中所谓定义一个函数的时候,我们只是新建了一个变量,然后这个变量里面保存了一个函数对象,也就是 function object。我们可以看,这个 double 本身它就是一个 object。
可调用对象
那么函数对象有一个特点,就是它是一个 callable object。在 Python 里面,callable object 就是这个东西,你后面可以跟一个小括号,然后去调用它。那我们定义的这个函数呢,它还需要一个参数 x,所以在我们每次调用的时候呢,我们还需要给它一个参数。
实际上呢,在 Python 的语法层面,任何的对象你都可以调用,但是这个东西如果不是 callable 的话,它在 runtime 会出错。
import dis
dis.dis("1()")
我们看,我们把这个 1()进行编译,它也是可以编译通过的,只不过它会给你一个 SyntaxWarning,告诉你这个好像是错的。但是它的字节码表示,一样可以 load 完这个 1 之后,然后再尝试 call 它。
函数作为参数
那当你理解了在 Python 中,函数不过是一个普通的对象的时候,你就非常容易理解函数也可以被当作参数,传进其他的函数里。
比如在这里我们定义了一个 double,定义了一个 triple,那这两个保存着函数对象的变量,都可以被传进 calc_number 这个函数里。那 calc_number 这个函数呢,第一个参数是一个函数,它 take 一个函数,第二个参数是一个数,然后它就把这个函数对这个数进行一个调用。
def double(x):
return x * 2
def triple(x):
return x * 3
def calc_number(func, x):
print(func(x))
calc_number(double, 3)
calc_number(triple, 3)
当传进 double、3 的时候呢,它就会打印一个 double(3)。当传进 triple、3 的时候,它就会打印一个 triple(3)。我们运行一下,大家可以看,第一个打印出来的是 3×2,是 6;第二个打印的是 3×3,是 9。
所以函数并没有什么神奇的地方,它完全可以像其他的变量一样,被传来传去,被传进其他的函数。
函数作为返回值
那同样的,函数不光可以被作为变量传进其他的函数,函数本身也可以成为一个返回值。比如我们下面这个 get_multiple_func,它本身是一个函数,它返回的也是一个函数。而它返回的这个函数做什么事情,和 get_multiple_func 得到的参数有关。如果你给 get_multiple_func 的参数是 2,它就返回一个函数,这个函数把输入值乘 2 返回。
def get_multiple_func(n):
def multiple(x):
return n * x
return multiple
double = get_multiple_func(2)
triple = get_multiple_func(3)
print(double(3))
print(triple(3))
这块稍微有点绕啊,一定要看明白。get_multiple_func take 一个 argument,是 n。它返回的这个函数是 multiple,multiple 也是一个函数。而 multiple 返回什么值,是由这个 get_multiple_func 的这个参数 n 决定的,它返回的是 n 乘 x。因此 get_multiple_func(2) 返回的就是 double 这一个函数,get_multiple_func(3) 返回的就是 triple 这一个函数。
而这两个函数都是可被调用的,也就是都是 callable。我们可以看到,下面我们就在正常地使用这两个函数,跟之前我们 define 的那个 double 跟 triple 一样。double 括号 3,triple 括号 3。我们运行一下,我们可以看,依然是输出的 6 和 9。
装饰器语法
好,那如果你能理解函数可以作为一个参数被传进其他函数,你也可以理解函数的返回值可以是一个函数,那么 decorator 相对来说就变得非常容易理解了。
好,那我们现在写了一个极简的 decorator。首先你要知道的第一件事就是 decorator 本身就是一个 callable,它也没有特殊的地方。那在最开始的时候,你可以理解为 decorator 本身就是一个函数,也就是 @ 后面跟的这个名字,它就是一个函数名。我们可以看到,我们定义了一个函数叫做 dec,然后我们把这个 double 上面加了一个 @dec。
import dis
s = """
def dec(f):
pass
@dec
def double(x):
return x * 2
"""
dis.dis(s)
接着我们来看上面的字节码,上半部分没有任何特殊的地方,就跟我们刚才定义一个函数是长得一模一样的。那这个部分就是这个 decorator 真正起作用的部分。我们可以看到,中间和刚才定义那个函数依然是一样的,中间定义的是一个 double 函数。而真正的 decorator 起效果的是这个 CALL在这段字节码中,原函数占用隐式参数槽位,不计入 CALL 后的数字;CALL 0 仍会把它传给 dec。 跟 STORE_NAME把栈顶的对象绑定到当前命名空间中的指定名字。,和前面的这个 LOAD_NAME dec 一起看,我们就知道 decorator 本身只是一个语法糖。它和下面这个写法是一模一样的。
def dec(f):
pass
def double(x):
return x * 2
double = dec(double)
当我们对一个函数做一个 decorator 的操作的时候,它完全等价于这个变量名等于 decorator 调用这个变量名。也就是说,decorator 不过是一个输入和输出都是函数的函数。
装饰器的返回值
当然了,你说输入输出都一定是函数吗?输入一定是,输出还真不一定是。为了帮助大家理解,我们举这么一个极端的例子。
我们看下面的代码,没有人这么写过代码吧?没有人 decorator return 过一个 1,对不对?我们的 decorator return 是一个 1,然后我们把这个 double 上面加上一个 decorator,然后 print double。我们看看结果是什么。很简单,就是 1 啊。
def dec(f):
return 1
@dec
def double(x):
return x * 2
print(double)
注意啊,我们 print 是 double 这个变量本身,而不是对 double 进行任何的调用,不是什么 double 括号 2、括号 3 之类的。为什么 double 变成 1 了呢?
def dec(f):
return 1
def double(x):
return x * 2
double = dec(double)
print(double)
很简单啊,我们说这个部分和它是等价的,对不对?那如果 dec 返回的是 1 的话,那么 dec(double) 返回的是 1,1 被赋值给了 double,print 1 就是 1 啊。
那这个例子也是希望告诉大家,decorator 这个东西本身没有任何的神秘性,它就是一个语法糖而已。好,那当然了,在绝大部分的情况下,我们使用 decorator 的时候,还是认为 decorator 是一个参数是函数、返回值也是函数的这么一个函数。
函数计时
好,让我们来看这个例子。那为了减少大家的发蒙程度呢,我把 args 跟 keyword args 先给拿掉了。我们可以看到 timeit 是一个函数,对吧?我们把它当作 decorator 来用,它 take 的这个 f 是一个函数,它返回这个 wrapper 还是一个函数。
那 wrapper 这个函数能干嘛呢?wrapper 这个函数首先和 my_func 一样,都 take 一个 argument。然后它刚开始计一个时,然后调用 timeit 给进来的这个参数 f,记录返回值,打印函数用时,然后返回返回值。
import time
def timeit(f):
def wrapper(x):
start = time.time()
ret = f(x)
print(time.time() - start)
return ret
return wrapper
@timeit
def my_func(x):
time.sleep(x)
my_func(1)
好,那我们看一下,这个 decorator 加上之后有什么效果。可以看到呢,它就是起到了一个计时,并且把这个函数运行时间打印出来这么一个功能。那使用 decorator 的好处呢,就是当你有不同的函数的时候,你都可以比较简洁地来完成这件事。
复用装饰器
比如说我们还有一个函数,我们有一个 other_func,对吧?这个函数返回一个 x 乘以 2。我们把它加上一个 decorator 之后呢,就会有这个效果。它会告诉你,运行这个函数花了这么长时间,然后返回值是在最后这行 print 上被打印的。
import time
def timeit(f):
def wrapper(x):
start = time.time()
ret = f(x)
print(time.time() - start)
return ret
return wrapper
@timeit
def my_func(x):
time.sleep(x)
@timeit
def other_func(x):
return x * 2
print(other_func(2))
可变参数
好,那我们回到它的初始形态。为什么这块用*args、**kwargs 呢?这两个函数参数,大家可以去自己查一下。本质上呢,它们就是允许变长的函数参数。这样的话,不管我装饰的是什么函数,它都能用。
那刚才那个 decorator,只能用在只有一个参数的函数上。现在比如说这个函数可能有若干个参数,像这样,这样也是可以用的。
import time
def timeit(f):
def wrapper(*args, **kwargs):
start = time.time()
ret = f(*args, **kwargs)
print(time.time() - start)
return ret
return wrapper
@timeit
def my_func(x):
time.sleep(x)
@timeit
def add(x, y):
return x + y
print(add(2, 3))
带参数的装饰器
好,那我们再进阶一下。有人说了,我还见过带参数的 decorator,那是怎么回事呢?其实带参数的 decorator 也没有任何神奇的地方。它就是相当于等价的过程中多了一次函数调用。
原来不带参数的时候是直接 timeit 括号 double,直接用 timeit 这个函数去调用 double,对不对?现在带了参数,它要先计算 timeit 括号 10,也就是这个参数。这个东西返回的是一个函数,这个函数再去调用 double,再返回一个函数。
它们两个现在变成完全等价的。原来这个 decorator 是参数是函数,返回值也是函数。现在这个 decorator 是参数是可以你任意指定的东西,然后它要返回一个函数 a,这个函数 a 的参数是函数,返回值也是函数。现在的这个 decorator 要返回一个原来的那种 decorator。
重复计时
好,我们看一下实现啊。最外层的这个我们使用的 decorator take 的是一个 argument,参数是运行多少次。而它返回的是一个函数,这个函数叫做 inner。这个 inner 是我们之前那个版本没有参数的 decorator,那个 decorator。我们可以看到,这个 inner 跟刚才一样,take 的是一个函数,返回的也是一个函数。而这个函数 wrapper 里面是运行了 iteration 次的函数 f。
import time
def timeit(iteration):
def inner(f):
def wrapper(*args, **kwargs):
start = time.time()
for _ in range(iteration):
ret = f(*args, **kwargs)
print(time.time() - start)
return ret
return wrapper
return inner
@timeit(1000)
def double(x):
return x * 2
double(2)
调用过程
那本质上它相当于运行了这么一个东西啊,首先 timeit(1000) 拿到了一个 inner 这个函数,然后 inner 对 double 做一个调用,返回了一个函数,保存到 double 里。
inner = timeit(1000)
def double(x):
return x * 2
double = inner(double)
double(2)
我们看一下运行结果,可以看到,这是跑 1000 次 double 所需要的时间。