Python 装饰器详解
1 引言
装饰器(Decorators)可能是Python中最难掌握的概念之一了,也是最具Pythonic特色的技巧,深入理解并应用装饰器,你会更加感慨——人生苦短,我用Python。
2 初步理解装饰器
2.1 什么是装饰器
在解释什么是装饰器之前,我们有必要回顾一下Python中的一些思想和概念。我们都知道,Python是一门面向对象的语言,Python基本思想就是一些皆对象,数据类型是对象、类是对象、类实例也是对象……对于接下来我们要说的装饰器而言,最重要的是,函数也是对象!
你没看错,函数也和数据类型等概念一样,都是对象,那么既然数据类型可以进行赋值操作,那么函数是不是也可以赋值呢?当然可以!
def do_something(): print('完成一些功能') if __name__ == '__main__': do = do_something do()
输出:
完成一些功能
看,原本我们定义的函数名是do_something,但我们把函数赋值给do后,也可以通过do()调用函数。不仅如此,函数当做参数传递给其他函数:
def do_something(): print('正在完成功能') def func(f): f() if __name__ == '__main__': func(do_something)
输出:
完成一些功能
正是因为Python中函数既可以赋值给其他变量名,也可以当做参数参数进其他函数,所以,上面的代码没有任何问题。
当然,毕竟被称呼为函数,有别有变量之类的概念,所以它也有自己的特性,例如在函数内部,还可以定义函数,甚至作为返回值返回:
def func(): def inner_func(): print('我是內部函数') return inner_func if __name__ == '__main__': fun1 = func() fun1()
输出结果:
我是內部函数
我们来总结一下函数的这几个特性:
-
可以赋值给其他变量;
-
可以作为参数传递给其他函数;
-
可以在内部定义一个函数;
-
可以当做返回值返回。
不要疑惑我为什么要着重说明Python中函数的这几个特性,因为装饰器中正是这几个特性为基石。为什么这么说呢?从本质上来说,装饰器就是一个函数,它也具有我们上面说到的4个特性,而且充分利用了这4个特性。装饰器接受一个普通函数作为参数,并在内部定义了一个函数,在这个内部函数中实现了一些功能,并调用了传递进来的函数,最后将内部函数作为返回值返回。如果一个函数把这个步骤全走了一遍,我们就可以认为,这个函数是一个装饰器函数,或者说装饰器。
我们来动手写一个装饰器:
def func(f): def inner_func(): print('{}函数开始运行……'.format(f.__name__)) f() print('{}函数结束运行……'.format(f.__name__)) return inner_func def do_something(): print('正在完成功能') if __name__ == '__main__': do_something = func(do_something) do_something()
输出结果:
do_something函数开始运行……
正在完成功能
do_something函数结束运行……
在上面代码中,我们将do_something方法作为参数传递给func方法,在func方法内部我们定义了一个inner_func方法,并在这个方法中添加了函数开始执行和结束执行的提示,在func方法最后,我们将inner_func作为参数返回,更加值得一说的是,我们重新将func函数的返回值赋给了do_something,所以,最后一行我们再次调用的do_something方法已经不再是最初的do_something函数,而是func方法内定义的inner_func函数,所以最后执行do_something函数时,会有函数开始执行和结束执行的提示功能,而后面再调用do_something函数时,也都直接使用do_something()。
正如你所预料,func函数就是一个装饰器,捋一捋你会发现,func函数把我们上面说过的所有特性、步骤全实现了。
如果你在别处见过Python装饰器的使用,你可能会疑惑,我们实现的func装饰器跟你见过的装饰器不都一样,因为在实际应用中,装饰器大多是与“@”符号结合起来使用。其实“@”符号所实现的功能就是 do_something = func(do_something)这行代码的功能。来,我们尝试一下使用“@”:
def func(f): def inner_func(): print('{}函数开始运行……'.format(f.__name__)) f() print('{}函数结束运行……'.format(f.__name__)) return inner_func @func def do_something(): print('正在完成功能') if __name__ == '__main__': do_something()
输出结果:
do_something函数开始运行……
正在完成功能
do_something函数结束运行……
之前我们知道,func函数就是一个装饰器,所以使用“@”符号时,我们只需要在被装饰的函数前面加上“@func”就表示该函数被func装饰器装饰,在需要处直接调用do_something函数即可。
2.2 为什么要用装饰器
在上面代码中,我们写了一个装饰器func,在这个装饰器中,使用装饰器的好处就已经初见端倪了。
(1)可以在不对被装饰函数做任何修改的前提下,给被装饰函数附加上一些功能。使用@func对do_something函数进行装饰时,我们没有对do_something函数的代码做什么的改变,但是被装饰后的do_something函数却多了开始运行和结束运行的功能。
(2)不改变被装饰函数的调用方式。在被装饰前,我们通过do_something()调用这个函数,被装饰后,还是通过do_something()调用这个函数。
(3)代码更加精简。在上面代码中,我们只是用@func装饰了do_something一个函数,但是如果有多个函数需要添加开始运行和结束运行的提示功能,如果不用装饰器,那么就需要对每一个函数进行修改,则工作量和需要修改的代码量……用了装饰器之后,只需要在需要添加这一功能的函数前面添加@func就可以了。
一言以盖之,装饰器可以在不改变原函数调用方式和代码情况下,为函数添加一些功能,使代码更加精简。
我们在写一个装饰器来加深一下理解。相比大家都写过代码来统计一个函数的运行时间的功能,我们使用装饰器来实现一下这个功能:
import time def timmer(f): def inner_func(): start_time = time.time() f() end_time = time.time() print('{}函数运行消耗时间为:{}'.format(f.__name__, end_time-start_time)) return inner_func @timmer def do_something(): print('do_something函数运行……') time.sleep(1) if __name__ == '__main__': do_something()
输出结果:
do_something函数运行……
do_something函数运行消耗时间为:1.000662088394165
在上面例子中,我们首先定义了一个计时装饰器timmer,当需要统计某个函数运行时间时,只需要在函数定义时,在前面添加一行写上@timmer即可,例如上面对do_something函数运行时间进行统计,对do_something原来要实现什么功能就继续实现这一功能,原来代码该怎样还怎样,该怎么调用还怎么调用。所以说,使用装饰器可以在不改变原函数代码和调用方式的情况下附加上其他功能。
如果你阅读到了这里,我想你对装饰器已经有了初步的理解。接下来,我们继续聊一聊更加复杂的装饰器。
3 深入理解装饰器
3.1 被装饰的函数带返回值
我们上面写的两个装饰器所装饰的do_something函数是没有返回值的,但大多数函数可都是有返回值的。针对有返回值的函数,装饰器该怎么写呢?
def func(f): def inner_func(): print('{}函数开始运行……'.format(f.__name__)) ret = f() print('{}函数结束运行……'.format(f.__name__)) return ret # 这里返回值 return inner_func @func def do_something(): print('正在完成功能') return '我是返回值' if __name__ == '__main__': ret = do_something() print(ret)
输出结果:
do_something函数开始运行……
正在完成功能
do_something函数结束运行……
我是返回值
我们知道,被装饰后的do_something函数其实不再是最初的do_something函数,而是装饰器内部定义的inner_func函数,所以,被装饰的函数的返回值只需要通过装饰器内部定义的inner_func函数返回返回即可即可。有点绕,不过对着上面的代码应该好理解。
3.2 被装饰函数带参数
对于装饰器,我们要深刻理解一件事:以上面的装饰器func和被装饰函数do_something为例,被装饰后的do_something函数已经不再是原来的do_something函数,而是装饰器内部的inner_func函数。这句话我已经在上文中我已经不止提过一次,因为真的很重要。如果被装饰的函数有参数(加入参数为name),我们还是通过do_something(name)的方式传递传输,不过,既然我们最终调用的时候,通过do_something实质调用的inner_func函数,那么在定义装饰器是,定义的inner_func函数时也需要接受参数。
def func(f): def inner_func(name): print('{}函数开始运行……'.format(f.__name__)) ret = f(name) print('{}函数结束运行……'.format(f.__name__)) return ret return inner_func @func def do_something(name): print('你好,{}!'.format(name)) return '我是返回值' if __name__ == '__main__': ret = do_something('姚明') print(ret)
输出结果:
do_something函数开始运行……
你好,姚明!
do_something函数结束运行……
我是返回值
一个装饰器可用于装饰千千万万个函数,则千千万万个函数参数情况可能各不相同,有的没有参数,有的可能多个参数,甚至还有关键字参数,对于这参数情况不同的函数,我们不可能为每个函数都写一个func装饰器,那怎么办呢?
Python中提供了*args, **kwargs这种机制来接受任意位置的位置参数和关键字参数,参数前面带*表示接受任意个数位置参数,接收到的所有位置参数存储在变量名为args的元组中,带**表示接受任意个数的关键字参数,接收到的所有关键字参数以字典的形式参数在变量名为kwargs的字典中。
当我们知道只有位置参数,但不知道有多少个位置参数是func装饰器可以这么写:
def func(f): def inner_func(*name): print('{}函数开始运行……'.format(f.__name__)) ret = f(*name) print('{}函数结束运行……'.format(f.__name__)) return ret return inner_func @func def do_something(name): print('你好,{}!'.format(name)) @func def do_something_2(name_1, name_2): print('你好,{}!'.format(name_1)) print('你好,{}!'.format(name_2)) @func def do_something_3(*name): for n in name: print('你好,{}!'.format(n)) if __name__ == '__main__': do_something('姚明') print('-------------------------------') do_something_2('姚大明', '姚小明') print('-------------------------------') do_something_3('姚一明', '姚二明', '姚三明', '姚四明')
输出结果:
do_something函数开始运行……
你好,姚明!
do_something函数结束运行……
-------------------------------
do_something_2函数开始运行……
你好,姚大明!
你好,姚小明!
do_something_2函数结束运行……
-------------------------------
do_something_3函数开始运行……
你好,姚一明!
你好,姚二明!
你好,姚三明!
你好,姚四明!
do_something_3函数结束运行……
上面例子定义func装饰器时,我们用*name来接受任意个数的位置参数,可别以为只能用*args,args只是一个变量名,只不过约定俗成,用的多一些,实际开发时你爱取啥名就用啥名,对于这个知识点不再多说,毕竟本篇主角是装饰器。我们继续装饰器内容!
当我们知道只有关键字参数,却不知道参数个数时,可以func装饰器这么写:
def func(f): def inner_func(**name): print('{}函数开始运行……'.format(f.__name__)) ret = f(**name) print('{}函数结束运行……'.format(f.__name__)) return ret return inner_func @func def do_something(name='无名氏'): print('你好,{}!'.format(name)) @func def do_something_2(name_1='无名氏', name_2='无名氏'): print('你好,{}!'.format(name_1)) print('你好,{}!'.format(name_2)) @func def do_something_3(**name): for n in name.keys(): print('你好,{}!'.format(name[n])) if __name__ == '__main__': do_something(name='姚明') print('-------------------------------') do_something_2(name_1='姚大明', name_2='姚小明') print('-------------------------------') do_something_3(name_1='姚一明', name_2='姚二明', name_3='姚三明', name_4='姚四明')
输出结果:
do_something函数开始运行……
你好,姚明!
do_something函数结束运行……
-------------------------------
do_something_2函数开始运行……
你好,姚大明!
你好,姚小明!
do_something_2函数结束运行……
-------------------------------
do_something_3函数开始运行……
你好,姚一明!
你好,姚二明!
你好,姚三明!
你好,姚四明!
do_something_3函数结束运行……
事实上,大多数情况下,我们对被装饰函数是一无所知的——我们不知道有多少个位置参数、多少个关键字参数,甚至对有没有位置参数、关键字参数都不知道,这时候,我们就只能*args和**kwargs齐上阵了:
def func(f): def inner_func(*name1, **name2): print('{}函数开始运行……'.format(f.__name__)) ret = f(*name1, **name2) print('{}函数结束运行……'.format(f.__name__)) return ret return inner_func @func def do_something(name): print('你好,{}!'.format(name)) @func def do_something_2(name_1, name_2='无名氏'): print('你好,{}!'.format(name_1)) print('你好,{}!'.format(name_2)) @func def do_something_3(*name1, **name2): for n in name1: print('你好,{}!'.format(n)) for n in name2.keys(): print('你好,{}!'.format(name2[n])) if __name__ == '__main__': do_something(name='姚明') print('-------------------------------') do_something_2(name_1='姚大明', name_2='姚小明') print('-------------------------------') do_something_3('姚一明', '姚二明', '姚三明', name_4='姚四明')
输出结果:
do_something函数开始运行……
你好,姚明!
do_something函数结束运行……
-------------------------------
do_something_2函数开始运行……
你好,姚大明!
你好,姚小明!
do_something_2函数结束运行……
-------------------------------
do_something_3函数开始运行……
你好,姚一明!
你好,姚二明!
你好,姚三明!
你好,姚四明!
do_something_3函数结束运行……
3.3 装饰器本身带参数
我们上面写的装饰器都没有参数,或者说只有一个自带参数,也就是被装饰函数f。其实,装饰器也是可以有其他参数的,这样的装饰器更加灵活。我们通过实例来说明:现在我们要对上面的func装饰器进行改进,需要做到灵活控制装饰器是用中文输出还是用英文输出,代码如下。
def language(lang='中文'): # 这里带参数 def func(f): # 往里嵌套了一层 def inner_func(*name1, **name2): if lang=='中文': print('{}函数开始运行……'.format(f.__name__)) else: print('The function of {} starts runging…'.format(f.__name__)) ret = f(*name1, **name2) if lang=='中文': print('{}函数结束运行……'.format(f.__name__)) else: print('The function of {} ends runging…'.format(f.__name__)) return ret return inner_func return func @language('中文') def do_something(name): print('你好,{}!'.format(name)) @language('English') def do_something_2(name): print('你好,{}!'.format(name)) if __name__ == '__main__': do_something(name='姚明') print('-------------------------') do_something_2(name='姚明')
输出如下:
do_something函数开始运行……
你好,姚明!
do_something函数结束运行……
-------------------------
The function of do_something_2 starts runging…
你好,姚明!
The function of do_something_2 ends runging…
可以看到,通过装饰器带参数的方式,我们只需要在定义被装饰函数时,指定装饰器参数,就可以灵活控制每个被装饰函数提示的语言。
当然,必须承认,装饰器带参数后,看起来更加复杂,需要多嵌套一层函数,由最外层的函数接受参数,里层函数才是真正的装饰器。使用装饰器时,会首先运行带参数的最外层函数,返回装饰器,这一步Python会自动帮我们完成。所以,带参数的装饰器甚至可以这么使用:
h = language('中文') @h def do_something(name): print('你好,{}!'.format(name))
3.4 多层装饰器
装饰器也是可以多层嵌套使用的,也就是说,一个函数可以通过是被多个装饰器所装饰,执行顺序是从下到上的优先顺序加载装饰:
# -*- coding: utf-8 -*- import time print(1) def func(f): print(2) def inner_func(*name1, **name2): print('{}函数开始运行……'.format(f.__name__)) f(*name1, **name2) print('{}函数结束运行……'.format(f.__name__)) print(3) return inner_func print(4) def timmer(f): print(5) def inner_timmer(*args, **kwargs): print('开始计时……') start_time = time.time() f(*args, **kwargs) end_time = time.time() print('开始结束……'上一篇: 什么是 Python 装饰器?
推荐阅读
-
2048小游戏代码的每步解析python 2048游戏代码及详解
-
深度解析Python强大的编辑器PyCharm:完整使用指南!
-
学习Python图像处理的必备技能:傅里叶变换原理及实现详解
-
详解Python scipy.signal.windows.gaussian的使用方法-参数解析
-
Egit使用方法详解——版本控制器助手
-
探索函数对象、函数嵌套、名称空间、作用域和装饰器
-
对推断原因进行分析:移动conda环境后出现“/home/×××/bin/python”缺少解析器的错误
-
详解6个实用的Python自动化脚本
-
30. LVS调度器详解 - 第一节
-
入门机器学习:Sklearn数据集、转换器和预测器详解