Python的装饰器一直是Python编程中比较难的知识点之一,虽然网上有很多讲解装饰器的文章,但始终觉得不够通俗易懂,看得云里雾里。本文将尝试以比较通俗易懂的方式来讲解装饰器的基本原理和用法,并在文末总结了装饰器的常见编写套路,按照这些套路,大家基本都能按要求写出各种装饰器。总之,本文的目的只有一个,希望大家看完本文之后,妈妈再也不担心你不会使用装饰器了。

【装饰器的定义及原则】

装饰器定义:所谓“器”,本质是函数。装饰器的功能本质是用来装饰其他函数的,即为其他函数添加特定附加功能的函数。

装饰器编写的两大基本原则

  1. 不能修改被装饰的函数的源码
  2. 不能修改被装饰的函数的调用方式

实现装饰器的前置知识条件

  1. 函数即变量
  2. 掌握高阶函数的相关知识
  3. 掌握函数嵌套的相关知识

装饰器 = 高阶函数 + 嵌套函数

下面来依次看看这些知识点。

函数即变量

  1. 任何变量名都是指向变量值的内存地址。如果把存放一个变量值的空间看成是一间屋子的话,那么这间屋子里存放的就是变量的值,而变量名就是屋子上的门牌号。
  2. 对于函数也是一样的道理,函数的本质是一串字符串,这串字符串会保存在内存空间中,函数名是指向这个内存空间的地址,也相当于是一个门牌号。

结论:函数和普通变量的存储原理是一致的,函数名可以像变量名那样使用,比如可以进行赋值等,例如下面这个简单的例子:

  1. def foo():
  2. pass()
  3. def boo():
  4. pass()

对于上面两个不同的函数来说,如果执行下面操作:

x = foo # 将foo函数的内存地址赋值给x变量,则x变量此时也指向foo函数的内存地址

foo = boo # 将boo函数的内存地址赋值给foo变量,则此时foo就不再指向以前foo函数的内存地址,而改为指向boo函数的内存地址,运行foo()将等同于执行boo()

  1. # 观察下面函数运行结果
  2. # def foo():
  3. # print("in foo")
  4. # def boo():
  5. # print("in boo")
  6. # foo()
  7. # boo()
  1. in boo
  2. in foo

上面这段代码的执行过程如下:

  • 程序自上而下依次运行。首先,将foo函数的定义以“字符串形式”加载到内存,“foo”指向该内存的地址。注意:所有函数定义阶段的代码都是以字符串形式加载到内存中的,所以在定义阶段遇到的任何需要执行的语句,比如调用其他函数的语句,其实都不会立即执行,这个概念非常重要。
  • 然后程序会将boo函数的定义也以字符串形式加载到内存中,“boo”指向该内存的地址。
  • 然后,程序遇到“boo()”,将开始执行之前加载的boo函数的代码,打印完“in boo”后,程序遇到“foo()”,就开始去寻找foo指向的内存中的代码。找到后,就开始执行这段代码。打印出“in foo”后,最终代码执行完毕。
  1. # def boo():
  2. # print("in boo")
  3. # foo()
  4. # def foo():
  5. # print("in foo")
  6. # boo()
  1. in boo
  2. in foo

在上面这段代码中,程序自上而下执行会首先将boo函数的代码加载到内存中,根据上面讲的知识,当遇到boo函数内的“foo()”语句时,仅仅是加载这个字符串,并非开始执行foo函数。接下来程序又把foo函数的函数体加载到内存中,最后遇到“boo()”语句,再去执行内存中boo函数对应的代码。此时打印完“in boo”后,程序会开始查找内存中foo函数的定义。由于之前已经在内存中加载了,所以能够顺利执行。

要点:

  • 在函数定义中去调用其他函数时,并不会立即调用该函数,记住函数定义在内存中仅仅是一串字符串而已,此时被调用函数有没有定义并没有任何关系。
  • 在执行一个调用了其他函数的函数时,如果在内存中还没有找到被调用函数的定义,则程序会报错
  1. def boo():
  2. print("in boo")
  3. foo()
  4. boo()
  5. def foo():
  6. print("in foo")

在上面这段代码中,程序首先加载boo函数的定义到内存中,完了直接执行了boo方法,此时打印完“in boo”后,程序通过“foo()”去找内存中foo对应的函数定义,结果发现找不到,因为foo函数是执行boo函数后才定义的,程序执行boo函数时,foo函数的代码还没有加载到程序中,所以内存中并没有存在“foo”这个门牌号的代码空间,所以上面这段代码会报错。

理解了以上这些原理之后,我们再接着看第二个知识点:高阶函数

高阶函数

  1. 什么样的函数是高阶函数,符合下列条件之一即称为“高阶函数”
  • 条件一、接受函数名作为形参
  • 条件二、返回值中包含函数名

符合条件一的高阶函数举例:

  1. def foo():
  2. print('in foo')
  3. def gf(func):
  4. print(func)
  5. func()
  6. gf(foo)

在这段代码中,我们定义了一个函数gf,并且以一个函数名作为形参,所以它是一个高阶函数。然后,在调用gf时,我们将foo函数的函数名以实参形式传入。运行以上代码,我们会发现,运行gf函数后执行的效果与直接运行foo函数的效果是一致的,并且通过“print(func)”打印出了实参对应的函数的内存地址。
那么这个例子有什么意义呢?这就给我们一个启示,通过以函数名作为形参的高阶函数,可以帮我们运行实参对应的函数的同时,再运行附加的特定代码实现特定的功能。比如下面这个例子:

  1. import time
  2. def foo():
  3. time.sleep(3)
  4. print('in foo')
  5. def gf(func):
  6. start_time = time.time()
  7. func()
  8. end_time = time.time()
  9. print('运行func的时间为{}'.format(end_time - start_time))
  10. gf(foo)

运行这段代码,我们会在正常运行foo函数的同时,打印出foo函数的运行时间。这其实已经实现了装饰器的作用,即为其他函数添加新功能,并且遵循了装饰器的第一个原则——不改变被装饰函数(在上面代码中是foo)的源码。但它还不是真正的装饰器,因为它改变了被装饰函数的调用方式。那怎么样才能不改变被装饰函数的调用方式呢?貌似我们只能在gf中返回func的地址,然后将gf返回的值重新赋值给foo,而不是在gf中调用foo函数。我们来看看代码:

符合条件二的高阶函数举例:

  1. def foo():
  2. time.sleep(3)
  3. print('in foo')
  4. def gf(func):
  5. start_time = time.time()
  6. return func
  7. end_time = time.time()
  8. print('运行func的时间为{}'.format(end_time - start_time))
  9. foo = gf(foo)
  10. foo()

运行上面这段代码,我们发现,虽然foo还是能正常运行,但似乎原函数没有任何改变,我们统计运行时间的效果也没有了。这是什么原因呢?因为在执行gf高阶函数的过程中,首先记录了start_time,然后程序就直接返回了func的内存地址,后面的代码并没有执行,所以后面的结束时间和打印语句都没有运行,自然就得不到函数运行时间了。这看起来似乎没有什么“卵用”,但这个说明了一点,就是我们可以通过在高阶函数中返回函数名,实现不修改函数调用方式的目的。

总结一下高阶函数部分的知识,对于高阶函数的两个条件条件来说,这两个条件对于构造装饰器的意义在于:

  • 条件一、接受函数名作为形参(在不改变被装饰函数源码的条件下为其增加功能)
  • 条件二、返回值中包含函数名(不改变被修饰函数的调用方式)

但现在这两个条件不能组合起来用,也就是说还无法真正实现之前说的装饰器的一个目的:为被装饰函数增加新功能及两个原则:不改变被装饰函数的源码、不改变被装饰函数的调用方式。那么究竟怎样才能编写真正的装饰器函数呢?根据之前我们写的公式,装饰器=高阶函数+嵌套函数,看来我们单靠高阶函数是无法实现真正的装饰器的,我们还需要学习最后一个知识点:嵌套函数

嵌套函数

什么是嵌套函数?
通过def关键字定义在另一个函数中的函数叫嵌套函数,比如下面这种:

  1. def foo():
  2. def boo():
  3. psss

在上面这段代码中,boo叫foo的嵌套函数。如果只是在一个函数中调用另外一个函数,不叫嵌套函数,一定要注意区别。比如下面这种叫调用函数:

  1. def foo():
  2. time.sleep(3)
  3. print('in foo')
  4. def boo()
  5. foo()

上面这种叫在boo中调用了foo函数,不是嵌套用法。

理解了嵌套函数的概念,根据我们上面写过的公式:装饰器=高阶函数+嵌套函数,接下来我们就利用高阶函数和嵌套函数来实现真正的装饰器。看看下面这个函数:

  1. import time
  2. def foo():
  3. time.sleep(3)
  4. print('in foo')
  5. def timer(func):
  6. def gf():
  7. start_time = time.time()
  8. func()
  9. end_time = time.time()
  10. print('func运行时间为:{}'.format(end_time - start_time))
  11. return gf
  12. foo = timer(foo)
  13. foo()

运行上面这段代码,我们会发现通过结合嵌套函数和高阶函数,我们已经实现了装饰器的目的,并且完全遵守了装饰器的两个基本原则。但通过直接赋值的形式进行操作还是比较繁琐,看起来有点怪。其实这里可以再简化一下,利用python为我们提供了装饰器语法糖,即通过@timer的形式加在被装饰函数的定义上面的方式来实现装饰器效果。简化后,代码如下:

  1. import time
  2. def timer(func):
  3. def gf():
  4. start_time = time.time()
  5. func()
  6. end_time = time.time()
  7. print('func运行时间为:{}'.format(end_time - start_time))
  8. return gf
  9. @timer
  10. def foo():
  11. time.sleep(3)
  12. print('in foo')
  13. foo()

大家可以看到,这下就跟我们平时看到的装饰器长得一模一样了。那么装饰器是怎么运行的呢?为了了解装饰器的运行流程,我们可以给这段代码打上断点(除了第一行import time之外,其他语句全部打上断点),来看看它的执行过程。

  1. 运行调试后,我们会发现首先程序来到第一个def timer(func)。
  2. 接着加载timer里面的语句,当然里面没有什么需要执行的。
  3. 然后程序跳到了@timer语句。@timer在这里等价于执行了这样一条语句:foo=timer(foo),即通过@timer的语法糖,程序自动帮我们timer返回的值赋给foo变量,相当于把timer里面定义的gf函数的地址赋值给了foo。既然@timer相当于是运行了foo=timer(foo),那么接下来应该就要运行timer了。
  4. 点击继续运行,果然程序来到了timer的里面,定位到了def gf()这个语句上。
  5. 然后继续执行,加载完gf函数的代码,程序就会来到return gf。
  6. 再继续,程序会执行foo()。大家注意,此时foo已经在之前被重新赋值为了gf的地址,所以这个时候程序是要执行gf方法里面的语句的。
  7. 所以继续运行,程序会来到start_time = time.time()语句。
  8. 完了,继续执行func()。func()就是我们之前定义的foo里面的语句,所以其实就是执行了原始的foo函数。
  9. 程序跳到time.sleep(3),睡眠3秒。
  10. 继续打印in foo。
  11. 下一步,程序返回gf中执行end_time = time.time()
  12. 打印func时间,程序结束。

这就是上面这段装饰器函数的完整执行过程。从这里,我们总结一下嵌套函数的作用,其实装饰器中间这个嵌套函数有两个作用,一个是在嵌套函数中加入我们要增加的功能,另一个是在嵌套函数中调用被装饰的函数。这两点都是在嵌套函数中实现的。完了之后,在外层的装饰器函数中返回嵌套函数的地址,最后将这个地址通过语法糖赋值给被装饰函数名。整个操作的中心思想就是想给被装饰函数找个新的“替身”,在这个“替身”函数中,不仅能够引用原来定义的被装饰函数,并且还能运行新功能代码,达到用“狸猫”换“太子”的目的。这就是一个最基本的装饰器的主要操作,这也是我们编写装饰器的核心步骤和原理。

上面讲的装饰器是最简单的装饰器,但在实际的使用中,我们通常会遇到以下几种情况:

【被装饰函数带参数的处理】

比如我们给被装饰的foo函数加一个参数,代码如下:

  1. import time
  2. def timer(func):
  3. def gf():
  4. start_time = time.time()
  5. func()
  6. end_time = time.time()
  7. print('func运行时间为:{}'.format(end_time - start_time))
  8. return gf
  9. @timer
  10. def foo(name):
  11. time.sleep(3)
  12. print('in foo', name)
  13. foo()

在上面那个最简单的装饰器例子中,被装饰函数是不带参数的。如果被装饰函数带参数,那么按照上面这样定义装饰器,运行时将会报错,因为被装饰函数带参数,但装饰器内的嵌套函数在进行调用时,并没有给它传参,自然就会报错了。那么怎么解决这个问题呢?我们来考虑下程序运行的过程。当程序运行语法糖@timer语句时,实际上相当于执行了foo=timer(foo),而timer(foo)返回了gf的地址,则相当于执行了foo=gf。那么在运行foo()时,相当于是运行了gf()。那如果运行foo需要加参数,比如foo(name),那就相当于是在gf()上加参数name。那按照这个逻辑,我们就只需要在定义gf时,也加上参数就行了,把上面的gf函数定义改成下面这样:

改完之后,我们再次运行代码,发现程序可以正常运行了。

【装饰器本身带参数】

装饰器本身带参数是什么意思呢?比如我们这样的程序要求timer即时有两种单位,一种是按照minutes进行计时,一种是按照seconds进行计时,并且可以在装饰器上通过参数进行指定,代码如下:

  1. import time
  2. def timer(func):
  3. def gf(\*args, \*\*kwargs):
  4. start_time = time.time()
  5. func(\*args, \*\*kwargs)
  6. end_time = time.time()
  7. print('func运行时间为:{}'.format(end_time - start_time))
  8. return gf
  9. @timer(timer_type='minutes')
  10. def foo(name):
  11. time.sleep(3)
  12. print('in foo', name)
  13. @timer(timer_type='seconds')
  14. def boo():
  15. time.sleep(3)
  16. print('in boo')
  17. foo()
  18. boo()

上面这段代码中,我写两个被装饰函数,每个被装饰函数的装饰器上通过一个参数指定了装饰器的类型。那么这样这个装饰器还能正常运行吗?通过运行代码,我们得到一个错误提示:TypeError: timer() got an unexpected keyword argument ‘timer_type’。通过这个错误提示,我们会发现实际上,这个参数是传给了timer,而我们之前指定的timer只接受一个函数名作为参数。那么我们不妨将timer的参数改为timer_type试试,代码如下:

  1. import time
  2. def timer(timer_type):
  3. print('timer type:',timer_type)
  4. def gf(\*args, \*\*kwargs):
  5. start_time = time.time()
  6. func(\*args, \*\*kwargs)
  7. end_time = time.time()
  8. print('func运行时间为:{}'.format(end_time - start_time))
  9. return gf
  10. @timer(timer_type='minutes')
  11. def foo(name):
  12. time.sleep(3)
  13. print('in foo', name)
  14. @timer(timer_type='seconds')
  15. def boo():
  16. time.sleep(3)
  17. print('in boo')
  18. foo()
  19. boo()

这次我们指定timer接受了timer_type参数,并且在程序中打印出timer_type的值,用来检查是否真的接收了timer_type的值。结果运行后,打印出了timer_type的值,但同时也报了一个错,NameError: name ‘func’ is not defined。对啊,func现在没地方定义了,自然程序就没法运行了。那么怎么办呢?实际上,在这个时候,我们还要再加一层嵌套函数来接收这个func参数才能解决这个问题,代码如下:(由于嵌套函数又多了一个,为了方便大家区分,我把之前定义的嵌套函数df改名为inner,现在我们新加的这个改名为outer)

  1. import time
  2. def timer(timer_type):
  3. print('timer type:',timer_type)
  4. def outer(func):
  5. def inner(\*args, \*\*kwargs):
  6. start_time = time.time()
  7. func(\*args, \*\*kwargs)
  8. end_time = time.time()
  9. print('func运行时间为:{}'.format(end_time - start_time))
  10. return inner
  11. return outer
  12. @timer(timer_type='minutes')
  13. def foo(name):
  14. time.sleep(3)
  15. print('in foo', name)
  16. @timer(timer_type='seconds')
  17. def boo():
  18. time.sleep(3)
  19. print('in boo')
  20. foo('bill gates')
  21. boo()

再次运行代码,发现我们通过这种方式能够接受timer_type参数,并且程序运行正常。那这里的@timer(timer_type=’minutes’)是什么意思呢?通过打断点的方式,我们可以发现,这里这句话等价于foo=timer(timer_type=’seconds’)(foo),那其实相当于做了两件事:一是以timer_type为参数调用了timer函数,打印timer type的值;二是以被装饰函数名为实参调用了outer函数,并将outer的返回值inner重新赋值给传入的被装饰函数名。这个语句结束后相当于形成了foo=inner的状态。接下来再运行foo()和boo(),就相当于运行了inner函数。后面的逻辑就跟之前讲的一样了。

【被装饰函数有返回值】

还有一种情况是被装饰的函数有返回值,比如我们之前写的foo改成像下面这样:

那像这样的代码我们运行后还能得到打印输出的结果吗?运行后,我们发现打印出来的是空值。为什么会是空值呢?还会要结合代码来进行分析。在之前写的装饰器代码中,实际上运行foo执行的是inner函数,而inner函数我们并没有返回值(inner函数中调用的原函数由于并没有接收返回值,所以返回值被丢掉了),所以这里打印出来必定就是空值了。那么怎么解决这个问题呢?其实很简单,我们可以在inner中接收原函数的返回值并通过inner返回回来即可,代码如下:

  1. import time
  2. def timer(timer_type):
  3. print('timer type:',timer_type)
  4. def outer(func):
  5. def inner(\*args, \*\*kwargs):
  6. start_time = time.time()
  7. res = func(\*args, \*\*kwargs)
  8. end_time = time.time()
  9. print('func运行时间为:{}'.format(end_time - start_time))
  10. return res
  11. return inner
  12. return outer
  13. @timer(timer_type='minutes')
  14. def foo(name):
  15. time.sleep(3)
  16. print('in foo', name)
  17. @timer(timer_type='seconds')
  18. def boo():
  19. time.sleep(3)
  20. print('in boo')
  21. print(foo('bill gates'))
  22. boo()

这样修改之后,运行print(foo(‘bill gates’))就可以打印出’bill gates’了。

【避免被修饰函数的name被修改】

关于这个是装饰器的一个附加知识点,很多文章上都介绍过,解决方式就是导入functools.wraps库,将@functools.wraps(func)作为装饰器放在调用被装饰函数的嵌套函数上即可。

最后,我们再总结一下,根据上面讲的这些知识和原理,以后我们在编写装饰器时,无非就是这样几个步骤和套路:

  1. 首先定义一个装饰器函数,并且接受一个函数名作为形参(该函数就是被装饰函数,通过语法糖可自动实现赋值,我们只需要定义一个函数名形参即可)
  2. 然后定义一个wrapper嵌套函数,这个嵌套函数负责两件事,一是将新增功能的代码写在里面,二是通过外层装饰器中传入的函数名调用被装饰函数。
  3. 在外层装饰器函数中返回wrapper函数的地址。
  4. 如果被装饰函数带参数,则需要在调用被装饰函数的wrapper函数中加上类似args, *kwargs的形参并在调用被装饰函数时传给被装饰函数。
  5. 如果装饰函数本身带参数,则需要在wrapper函数外面再增加一个outer_wrapper,并将被装饰函数的函数名传给outer_wrapper。装饰函数本身的参数则传给最外层的函数名。
  6. 如果被装饰函数带返回值,则在内部调用被装饰函数的wrapper函数中也要把返回值return回来。