Skip to main content

函数装饰器

这篇文章我们来讲 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 所需要的时间。