☞ ░ 前往老猿Python博客 https://blog.csdn.net/LaoYuanPython ░
一、引言
我们知道Python中的函数也是一种对象,可以被引用、赋值和作为函数的返回值等,在此基础上我们在《第13.2节 关于闭包》介绍了闭包相关的概念,包括函数嵌套、自由变量等内容,而函数装饰器正是基于闭包来实现的一种高级函数机制。实际上装饰器不只是有函数装饰器,还有类装饰器。
二、装饰器的作用
2.5.1、装饰器与画框的类比
如果将要装饰对象比喻为一幅画,则装饰器可以比喻为一个画框:
- 画框包括必需的四周边框,如函数装饰器的封闭函数或包装类的声明部分
- 画框可以增加可选的画框面板,对应封闭函数或包装类调用被装饰函数或被装饰类的实例方法前的代码
- 画框可以增加可选的画框背板,对应封闭函数或包装类调用被装饰函数或被装饰类的实例方法后的代码
- 画本身即对应嵌套函数内调用被装饰函数或被装饰类被调用的实例方法的内容
- 可以将画装到不同的画框内,装了不同画框的画在展现时除了画本身外会有不同的特征,但其本质的内容却没有变化,画框对画本身没有任何的改变,而画框的却非常容易更换。装了画框的画虽然有改变,但对外的呈现还是画本身。
2.5.2、装饰器的作用
装饰器的主要作用有如下几点:
- 在不改变原被装饰对象包括函数、类的情况下,可以在原函数、类实例方法调用前后增加额外的逻辑,并且对调用方无需进行任何改变,对程序的稳定性影响很小
- 可以对程序功能框架总体增强时快速简单实现,如对所有调用函数或类实例方法增加执行时间统计
- 装饰器比较适用于程序调试、程序功能增强等方面
三、一个函数装饰器的例子
我们定义一个可以跟踪函数执行情况的装饰器log,该装饰器用于记录函数名、执行前/后的时间、执行时的参数值信息、执行后的结果,有了这些信息就可以知道函数调用的时间消耗、调用时的参数数据和执行后的结果。
3.1、案例函数装饰器的定义代码
import time
def log(fn,*a,**k):
def addLog(*a,**k):
curtime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
print(f"{curtime}:start excute function '{fn.__name__}',args:{a if len(a) else 'No args'} {','+str(k) if len(k) else ' '}")
ret = fn(*a,**k)
curtime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
print(f"{curtime}:function '{fn.__name__}' excuted end,return value:{ret}")
return ret
return addLog
3.2、案例函数装饰器的使用代码
@log
def add(d1,d2,*dn):
ret = d1+d2
for d in dn:
ret += d
return ret
@log
def sub(d1,d2,*dn):
ret = d1-d2
for d in dn:
ret -= d
return ret
3.3、执行及输出
>>> add(1,2,3,4,5)
2020-07-30 11:18:27:start excute function 'add',args:(1, 2, 3, 4, 5)
2020-07-30 11:18:27:function 'add' excuted end,return value:15
15
>>>
四、装饰器的四种类型
上面介绍了一个函数装饰器的例子,其实除了例子所示的装饰函数的函数装饰器之外,还有其他三种类型的装饰器,具体请参考《https://blog.csdn.net/LaoYuanPython/article/details/111303395 你不一定全知道的四种Python装饰器实现详解》的介绍。
五、装饰器的缺点及解决方案
装饰器很具有Python特色,用起来感觉也很好,是否就十分完美了呢?实际上这是不可能的,装饰器作为一个隐身的对象,在其隐秘的角落里也存在有它的缺点。
当使用装饰器来装饰一个函数或类时,实际上返回的对象已经变成了装饰器对象,而不是被装饰的对象。这就会导致有些程序处理逻辑会发生变化,因为对象变化,导致对象的类型、名字都发生了变化,使用isinstance函数或object.__name__
等形式判断对象类型、名字、被装饰对象的文档字符串等处理时会出错,这是因为装饰器会覆盖被装饰对象的元数据信息。
例如:
>>>class decorateClass:
def __init__(self,fun):
self.fun=fun
def __call__(self, *a, **k):
print("执行被装饰函数")
return self.fun( *a, **k)
>>> def fun( *a, **k):
'''函数名fun,可以带可变个数参数'''
print(f"执行函数fun,参数:",a,k)
>>> fun.__doc__
'函数名fun,可以带可变个数参数'
>>> fun.__name__
'fun'
>>> @decorateClass
def fun( *a, **k):
'''函数名fun,可以带可变个数参数'''
print(f"执行函数fun,参数:",a,k)
>>> fun.__doc__
>>> fun.__name__
Traceback (most recent call last):
File "<pyshell#31>", line 1, in <module>
fun.__name__
AttributeError: 'decorateClass' object has no attribute '__name__'
上面案例中,通过直接定义函数可以输出正常函数的文档字符串和函数名,而用装饰器装饰以后文档字符串变为了空,函数名则不能输出。为了解决问题,我们可以在装饰器中做些处理,如可以在类装饰器装饰函数时,在类装饰器中的构造方法中将需要保存装饰对象的属性直接赋值给类装饰器的实例变量,同样的如果是函数装饰器可以在封闭函数中做相同的处理。
下面是一个装饰函数的类装饰器的处理代码示例:
class decorateClass:
def __init__(self, fun):
self.fun = fun
self.__name__ = fun.__name__ #保存被装饰函数名
self.__doc__ = fun.__doc__ #保存被装饰函数文档字符串
def __call__(self, *a, **k):
print("执行__call__")
self.fun(*a, **k)
Python的functools模块提供了一个函数wraps也可以进行类似的处理,比自己去添加手动处理方便且考虑全面,不过可能并不适用所有四种类型的装饰器。具体相关介绍请参考:
六、装饰器作用的补充说明
前面介绍装饰器的作用时,主要是考虑到装饰器的通用性介绍的,当装饰器为类或被装饰对象为类时,老猿认为装饰器还有如下作用:
当被装饰对象为类时:
- 可以动态给被装饰类加相关属性,如类实例的个数计数前后内容;
- 可以捕获所有类方法的调用及属性的访问;
- 可以判断某个类是否具有某个属性,从而进行不同的处理。
当装饰器为类时:
- 如果被装饰对象为函数,可以将函数对象化,从而可以通过特定应用的对象判断;
- 如果被装饰对象为类,可以给被装饰类特定实例方法加前后处理逻辑。
七、小结
本文介绍了装饰器的类型、作用以及缺点及其解决方法,有助于大家深入理解Python3的装饰器机制。
更多关于装饰器的介绍请参考Python3的指南。
写博不易,敬请支持:
如果阅读本文于您有所获,敬请点赞、评论、收藏,谢谢大家的支持!
关于老猿的付费专栏
- 付费专栏《https://blog.csdn.net/laoyuanpython/category_9607725.html 使用PyQt开发图形界面Python应用》专门介绍基于Python的PyQt图形界面开发基础教程,对应文章目录为《 https://blog.csdn.net/LaoYuanPython/article/details/107580932 使用PyQt开发图形界面Python应用专栏目录》;
- 付费专栏《https://blog.csdn.net/laoyuanpython/category_10232926.html moviepy音视频开发专栏 )详细介绍moviepy音视频剪辑合成处理的类相关方法及使用相关方法进行相关剪辑合成场景的处理,对应文章目录为《https://blog.csdn.net/LaoYuanPython/article/details/107574583 moviepy音视频开发专栏文章目录》;
- 付费专栏《https://blog.csdn.net/laoyuanpython/category_10581071.html OpenCV-Python初学者疑难问题集》为《https://blog.csdn.net/laoyuanpython/category_9979286.html OpenCV-Python图形图像处理 》的伴生专栏,是笔者对OpenCV-Python图形图像处理学习中遇到的一些问题个人感悟的整合,相关资料基本上都是老猿反复研究的成果,有助于OpenCV-Python初学者比较深入地理解OpenCV,对应文章目录为《https://blog.csdn.net/LaoYuanPython/article/details/109713407 OpenCV-Python初学者疑难问题集专栏目录 》。
前两个专栏都适合有一定Python基础但无相关知识的小白读者学习,第三个专栏请大家结合《https://blog.csdn.net/laoyuanpython/category_9979286.html OpenCV-Python图形图像处理 》的学习使用。
对于缺乏Python基础的同仁,可以通过老猿的免费专栏《https://blog.csdn.net/laoyuanpython/category_9831699.html 专栏:Python基础教程目录)从零开始学习Python。
如果有兴趣也愿意支持老猿的读者,欢迎购买付费专栏。