Python高级编程——装饰器Decorator超详细讲解(上篇)
送你小心心记得关注我哦!!
进入正文
装饰器decorator,是python语言的重要特性,我们平时都会遇到,无论是面向对象的设计或者是使用相关的库,但是很少有文章对装饰器的来龙去脉进行深入详解,导致的结果就是很多人知其然,不知其所以然,所以我打算出一期关于Python装饰器的详解文章,由于内容较多,该文章共分为上下两篇,本文讲解上篇,下篇会在后面发出,请记得持续关注哦!文章偏长,阅读全文约20min。
全文目录
01 python装饰器诞生的背景
1.1 从一种简单的情况说起
1.2 非“装饰器”实现添加额外功能
02 什么是装饰器decorator
2.1 装饰器的两个层面的定义
2.2 装饰起的作用——两个方面
2.3 装饰器的使用场景
03 装饰器的实现
3.1 装饰器的逐步实现
3.2 装饰器的一般“模板”
04 装饰器的分类实现(下篇预告)
python装饰器详解(上篇)
01
python装饰器诞生的背景
装饰器的定义很是抽象,各个不同的文章有不同的表述,但是我不打算一开始就告诉你什么是Python的装饰器,这样显太过突兀,我们来看一个小例子。
先定义一个简单的函数:
def myfunc:
print('我是函数myfunc')
myfunc() #调用函数
然后呢,我想看看这个函数执行这个函数用了多长时间,好吧,那么我们可以这样做:
import time
def myfunc:
start = time.clock()
print('我是函数myfunc')
end = time.clock()
print(f'函数所花费的时间为 :{end - start}')
myfunc() #函数调用
我们现在已经达到了我们的目的。但是如果是我们还想继续给另外的一些函数也实现同样的功能。那我们是不是给每个函数都添加这么几句话呢?当然可以,但是不高效,而且很麻烦。如果有某一种方式可以一次性解决所有的问题,那自然最好不过了,于是“装饰器”就应运而生。
在上面的例子中,函数本身的功能只是打印一句话而已,但是经过改造后的函数不仅要能够打印这一句话,还要能够显示函数执行所花费的时间,这相当于我要给这个函数添加额外的功能,注意这个关键字,其实“装饰器”就是专门给函数添加额外的功能的。
还记得吗,函数在Python中是一等公民,那么我们可以考虑重新定义一个函数timeit,将myfunc的引用传递给他,然后在timeit中调用myfunc并进行计时,这样,我们就达到了不改动myfunc定义但是又添加了额外功能的目的,代码如下:
import time
def myfunc():
print("我是函数myfunc")
def timeit(function):
start = time.clock()
function()
end =time.clock()
print(f'函数执行所花费的时间为:{end-start}')
timeit(myfunc)
运行结果为:
我是函数myfunc
函数执行所花费的时间为:0.0004924657368762765
上面的代码看起来逻辑上并没有问题,也达到了我们所要实现的目的!但是,我们虽然没有修改函数myfunc定义中的代码,但是我们似乎修改了调用部分的代码。原本我们是这样调用的:myfunc(),修改以后变成了:timeit(myfunc)。这样的话,如果myfunc在N处都被调用了,你就不得不去修改这N处的代码。或者更极端的,考虑其中某处调用的代码无法修改这个情况,比如:这个函数是你交给别人使用的。
其实将函数作为参数传递,已经具备了装饰器的雏形了,但是上面的实现还不够好,下面会给出更好地实现方式。
02
什么是装饰器-decorator
一般而言,如果我需要给函数添加额外的某一些功能,我需要修改函数的源代码,但是如前面所说,这样麻烦,而且不高效,装饰器就是专门的解决方案!
装饰器的定义主要从两个不同层面进行说明的。
在Python里面有两层定义:
第一:从设计模式的层面上——代码重用
装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的应用有插入日志、增加计时逻辑来检测性能、加入事务处理等。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。
第二:从Python的语法层面上(其实第二种本质上也是第一种,只不过在语法上进行了规范化)
简言之,python装饰器就是用于拓展原来函数功能的一种函数,这个函数的特殊之处在于它的返回值也是一个函数,使用python装饰器的好处就是在不用更改原函数的代码前提下给函数增加新的功能。 如此一来,我们要想拓展原来函数功能,就不需要再在函数里面修改源代码了。
通过上面的表述,得出两个最基本的结论,其实这也是装饰器两个最基本的作用:
(1)抽离雷同代码,加以重用
(2)为函数添加额外的功能
装饰器可能在我们平时的编码中比较少去自己定义,更多的是我们使用别人的已经编写好的装饰器,比如我们我们经常使用的@staticmethod,@classmethod,@property等等,都是别人写好了,我们自己不需要自己再实现了,在编码中,我们在下面的一些情况会经常遇见装饰器。
(1)缓存装饰器
(2)权限验证装饰器
(3)计时装饰器
(4)日志装饰器
(5)路由装饰器
(6)异常处理装饰器
(7)错误重试装饰器
后面我还会讲到关于python的高级语法——python描述符(descriptor),其实也是跟python的装饰器有着千丝万缕的关系,详细可以参见后面的文章哦!
03
装饰器decorator的实现
针对上面改进版的代码所存在的哪些问题,我们想出了解决办法:
既然修改N处的调用代码很麻烦,我们就来想想办法不修改调用代码;如果不修改调用代码,也就意味着调用myfunc()需要产生调用timeit(myfunc)的效果。
因为python中一切皆对象,故而我们可以想到将timeit赋值给myfunc,
import time
def myfunc():
print("我是函数myfunc")
def timeit(function):
start = time.clock()
function()
end =time.clock()
print(f'函数执行所花费的时间为:{end-start}')
myfunc=timeit #将timeit赋值给原来的myfunc
myfunc()
但是上面的调用并不会成功,会显示出如下错误:
timeit() missing 1 required positional argument: 'function'
为什么呢?这是因为将timeit赋值给myfunc之后,此时myfunc和timeit表示的同一个东西,但是timeit似乎带有一个参数function,而在调用myfunc()的时候并没有传入任何参数,所以并不会成功。
但是上面的调用虽然没有成功,却给我们指出了一条重要的线索,因为上面的代码已经解决“修改调用代码”的问题,只不过是参数没有统一而已,那就想办法把参数统一吧!那就再添加一个函数呗!什么意思?
因为参数不统一,如果timeit()不并不是直接添加额外的功能,而是返回一个与myfunc参数列表一致的函数。而原来timeit需要添加额外功能的代码再在timeit里面定义一个函数,由它去完成不就可以了吗,将timeit(myfunc)的返回值赋值给myfunc,然后,调用myfunc()的代码完全不用修改。——即我们依然是调用myfunc(调用代码没变),但是同样却达到了添加额外功能的效果!
代码如下:
import time
#原来的函数myfunc
def myfunc():
print("我是函数myfunc")
#定义一个计时器
def timeit(function):
'''
timeit函数负责返回一个wrapper,wrapper的参数要与原来的myfunc保持相同
这样一来,执行 myfunc=timeit(myfunc) myfunc完全等价于wrapper
wrapper函数负责添加额外功能
'''
def wrapper():
start = time.clock()
function()
end =time.clock()
print(f'函数执行所花费的时间为:{end-start}')
return wrapper
myfunc=timeit(myfunc) #注意,这里与前面的 “myfunc=timeit”是有所区别的哦
myfunc() #还和原来调用myfunc()一样,但是达到了添加额外功能的效果
上面的运行结果就出来了,如下:
我是函数myfunc
函数执行所花费的时间为:0.0005973331798019136
总结:在上面的函数定义和调用中,看起来我的调用myfunc()和原来并没有任何不同,但是却已经添加了额外的效果。它解决前面存在的两个问题:
(1)不用修改函数源代码,也不用修改调用函数的代码,完全跟调用最原始的myfunc()代码一样,但是却添加了额外功能;
(2)解决了timeit和myfunc的参数不统一问题,那就是再添加一层wrapper;
——这就是装饰器。
上面的装饰器就是最原始的版本,但是python中引入了专门的“语法糖”来实现装饰器,这样看起来更加专业,更加美观。就是使用字符“@”去实现。代码如下:
import time
#定义一个计时器
def timeit(function):
'''
timeit函数负责返回一个wrapper,wrapper的参数要与原来的myfunc保持相同
这样一来,执行 myfunc=timeit(myfunc) myfunc完全等价于wrapper
wrapper函数负责添加额外功能
'''
def wrapper():
start = time.clock()
function()
end =time.clock()
print(f'函数执行所花费的时间为:{end-start}')
return wrapper
#myfunc=timeit(myfunc) #注意,这里与前面的 “myfunc=timeit”是有所区别的哦
#原来的函数myfunc
@timeit
def myfunc():
print("我是函数myfunc")
myfunc() #还和原来调用myfunc()一样,但是达到了添加额外功能的效果
上面代码的运行结果依然是:
我是函数myfunc
函数执行所花费的时间为:0.0004893814003196401
在上面的例子中,在定义myfunc函数的上面加了一个@timeit,这与前面的写法myfunc = timeit(myfunc)完全等价,
@有两个重要的作用,
第一:较少了代码书写量;
第二:那就是让我们的代码看上去更有装饰器的感觉,看起来更高端了。
总结
在这个例子中,函数进入和退出时需要计时,这被称为一个横切面(Aspect),这种编程方式被称为面向切面的编程(Aspect-Oriented Programming)。与传统编程习惯的从上往下执行方式相比较而言,像是在函数执行的流程中横向地插入了一段逻辑。在特定的业务领域里,能减少大量重复代码。面向切面编程还有相当多的术语,这里就不多做介绍,感兴趣的话可以去找找相关的资料(如果有需要,我后面也会抽时间专门写一系列关于面向切面编程的文章,看我有没有时间啦!)
为了能够明确装饰器的实现原理,这里给出一个关于装饰器的“一般模板”,方便大家理解!但是,装饰器作为一种设计模式,本身是没有固定的设计模板的,语法也是相对较为灵活,没有说一定要怎么写才正确。
模板如下:
def decorator(function):
'''
第一层函数为装饰器名称
function:参数,即需要装饰的函数
return:返回值wrapper,为了保持与原函数参数一致
'''
def wrapper(*arg,**args):
'''
内层函数,这个函数实现“添加额外功能”的任务
*arg,**args:参数保持与需要装饰的函数参数一致,这里用*arg和**args代替
'''
#这里就是额外功能代码
function() #执行原函数
#这里就是额外功能代码
return wrapper
一般就按照上面这个模板写“装饰器”函数,一般就不会出错了。
注意事项:
(1)装饰器一般由两层函数组成,外层的decorator,和内层的wrapper;
(2)第一层函数的参数function,即需要装饰的函数,返回值wrapper,为了保持与原函数参数一致
(3)内层函数,这个函数实现“添加额外功能”的任务, *arg,**args:参数保持与需要装饰的函数参数一致,这里用*arg和**args代替。
04
装饰器的各种花式实现
学过装饰器的人都知道Python的闭包,关于“闭包”的详细定义有各种版本,但我们经常看见这样一句话,“Python的装饰器就是一种闭包或者是Python的闭包其实就是装饰器”,这句话在一定程度上是不正确的,但是这么说也可以(心里要明白二者的本质)。
本质:python闭包是装饰器的真子集,即装饰器是更加宽泛的概念,至于为什么,它们二者的区别和联系,我会在下一篇文章里面详细说明。下一篇参考:
Python高级编程——装饰器Decorator详解(下篇)
不仅如此,上面所实现的装饰器是针对函数的,实际上Python的装饰器可以是“函数”或者是“类”,而被装饰的对象也可以是“函数”或者是“类”,这样一来,就有四种搭配情况,即:
函数装饰函数
函数装饰类
类装饰函数
类装饰类
具体每一种怎么实现呢?其实他们的设计思想都是大同小异,只是实现细节略有不同,欲知详细情况,且听下回分解!!!
下一篇预告:
装饰器与闭包的联系和区别
四大类装饰器的搭配实现
····
本文后面分享三本python使用的电子书,分别是
《python面试宝典》《python核心编程第三版》《利用python进行数据分析第二版》
下载链接:
链接:https://pan.baidu.com/s/11DLdpmc32Jm4SF3DCVB2Yg
提取码:5acm
····
推 荐 阅 读
一道人人都会的python题,考验你的编程思维,教你打开正确的学习方法
python开发者必备,史上最详细的 jupyter 教程——第三篇
python开发者必备,史上最详细的 ipython 教程——第一篇