写点什么

你不一定全知道的四种 Python 装饰器实现详解

用户头像
老猿Python
关注
发布于: 2021 年 04 月 23 日
你不一定全知道的四种Python装饰器实现详解

前往老猿Python博客

一、引言

老猿一直想写一篇比较完整的装饰器介绍的博文,从开始写到现在至少过去有半年了,一直都还未写完,因为还有一些知识点没有研究透彻,因此一直在草稿箱放着。在写这个材料的时候,发现 Python 中的装饰器网上介绍的材料很多,大多数都是介绍的装饰器函数,但也有极少数介绍了装饰器类或用装饰器函数装饰类。老猿想来,装饰器按照装饰器本身和被装饰对象来区分类和函数,应该有 4 种组合,前面说的有三种,应该还有一种装饰器和被装饰对象都是类的组合。但公开资料中未查到是否可以有类的类装饰器,即装饰器和被装饰对象都是类。老猿参考类的函数装饰器、函数的类装饰器做了很多测试,一度以为没办法实现类的类装饰器,准备放弃,隔了很长一段时间后,最近又花了两天时间进行研究测试,终于弄通了。基于上述研究,老猿决定先单独写一篇关于装饰器四种类型的详细介绍。


装饰器的概念就不介绍了,按照装饰器的类型、被装饰对象的类型,老猿将装饰器分为如下四种:


  1. 函数的函数装饰器:装饰器和被装饰对象都为函数;

  2. 类的函数装饰器:装饰器为函数,被装饰对象为类;

  3. 函数的类装饰器:装饰器为类,被装饰对象为函数;

  4. 类的类装饰器:装饰器和被装饰对象都为类。

二、函数的函数装饰器

装饰器包含装饰对象和被装饰对象,最简单的装饰器是用装饰器函数装饰被装饰函数,在这种场景下,装饰器为函数装饰器,被装饰对象也是函数。

2.1、概述

函数装饰器就是一个特殊的函数,该函数的参数就是一个函数,在装饰器函数内重新定义一个新的函数,并且在其中执行某些功能前后或中间来使用被装饰的函数,最后返回这个新定义的函数。装饰器也可以称为函数的包装器,实际上就是在被装饰的函数执行前或后增加一些单独的逻辑代码,以使得被装饰函数执行后最终的结果受到装饰函数逻辑的影响以改变或限定被装饰函数的执行结果。

2.2、装饰器定义语法


@decoratorNamedef originalFunction(*args,**kwargvs):
函数体
复制代码

2.3、装饰器语法解释

  • 装饰器的定义是以 @符号开头来声明的

  • decoratorName 是装饰器的名字,decoratorName 必须对应存在一个封闭函数(请参考《第13.2节 关于闭包》的介绍),该封闭函数满足如下要求:


  1. 参数是一个函数对象;

  2. 封闭函数内部存在一个嵌套函数,该嵌套函数内会调用封闭函数参数指定的函数,并添加额外的其他代码(这些代码就是装饰);

  3. 嵌套函数的参数必须包含 originalFunction 的参数,但不能带被装饰对象 originalFunction;

  4. 嵌套函数返回值必须与封闭函数参数指定函数的返回值类似,二者符合鸭子类型要求(关于鸭子类型请参考《第7.3节 Python特色的面向对象设计:协议、多态及鸭子类型》);

  5. 封闭函数的返回值必须是嵌套函数。


  • 装饰器函数的定义参考如下形式:



def decoratorName(originalFunction,*args,**kwargvs):
def closedFunction(*args,**kwargvs): ... #originalFunction函数执行前的一些装饰代码 ret = originalFunction(*args,**kwargvs) ... #originalFunction函数执行的一些装饰代码 return ret
return closedFunction
复制代码


其中 decoratorName 是装饰器函数,originalFunction 是被装饰的函数,closedFunction 是装饰器函数内的嵌套函数。


  • 装饰器定义的语法本质上等同于如下语句:

  • originalFunction = decoratorName(originalFunction)

2.4、多层装饰器的使用

在一个函数外,可以顺序定义多个装饰器,类似如:



@decorator1@decorator2@decorator3def originalFunction(*args,**kwargvs): 函数体
复制代码


这种多个装饰器实际上就是叠加作用,且在上面的装饰器是对其下装饰器的包装,以上定义语句效果等同于如下语句:



originalFunction = decorator3(originalFunction)originalFunction = decorator2(originalFunction)originalFunction = decorator1(originalFunction)
复制代码


也即等价于:



originalFunction = decorator1(decorator2(decorator3(originalFunction)))
复制代码

三、类的函数装饰器

3.1、定义

函数装饰器除了给函数加装饰器(使用函数名作为装饰器函数的参数)外,还可以给类加函数装饰器,给类加函数装饰器时,将类名作为装饰器函数的参数,并在装饰器函数内定义一个类如 wrapClass,该类称为包装类,包装类的构造函数中必须调用被装饰类来定义一个实例变量,装饰器函数将返回包装类如 wrapClass。

3.2、类的函数装饰器案例 1


def decorateFunction(fun, *a, **k): class wrapClass(): def __init__(self, *a, **k): self.wrappedClass=fun(*a, **k)
def fun1(self,*a, **k): print("准备调用被装饰类的方法fun1") self.wrappedClass.fun1(*a, **k) print("调用被装饰类的方法fun1完成")
return wrapClass
@decorateFunctionclass wrappedClass:
def __init__(self ,*a, **k): print("我是被装饰类的构造方法") if a:print("构造方法存在位置参数:",a) if k:print("构造方法存在关键字参数:",k) print("被装饰类构造方法执行完毕")
def fun1(self,*a, **k): print("我是被装饰类的fun1方法") if a:print("fun1存在位置参数:",a) if k:print("fun1存在关键字参数:",k) print("被装饰类fun1方法执行完毕")
def fun2(self,*a, **k): print("我是被装饰类的fun2方法")
复制代码


针对以上被装饰函数装饰的类 wrappedClass,我们执行如下语句:



>>> c1 = wrappedClass('testPara',a=1,b=2)我是被装饰类的构造方法构造方法存在位置参数: ('testPara',)构造方法存在关键字参数: {'a': 1, 'b': 2}被装饰类构造方法执行完毕
>>> c1.fun1()准备调用被装饰类的方法fun1我是被装饰类的fun1方法被装饰类fun1方法执行完毕调用被装饰类的方法fun1完成
>>> c1.fun2()Traceback (most recent call last): File "<pyshell#37>", line 1, in <module> c1.fun2()AttributeError: 'wrapClass' object has no attribute 'fun2'
>>>
复制代码


可以看到被装饰类的相关方法必须在装饰类中调用才能执行,装饰后的类如果装饰函数定义类时未定义被装饰类的同名函数,在装饰后返回的类对象无法执行被装饰类的相关方法。

3.3、类的函数装饰器案例 2

上面的案例 1 是通过将被装饰类的方法在装饰器函数内部的装饰类中静态重新定义方式来实现对被包装类方法的支持,这种情况可以用于装饰器装饰后的类只需调用指定已知方法,但有时我们的装饰器可能用于装饰多个类,只针对构造方法和特定方法在装饰类中重写会导致被装饰类需要调用的功能不能调用,这时我们需要在装饰器中实现一个通用方法来保障被装饰类装饰后能执行被装饰类的所有方法。这就需要借助 setattr 进行类实例方法的动态定义。



def decorateFunction(fun, *a, **k): class wrapClass(): def __init__(self, *a, **k): self.wrappedClass=fun(*a, **k) self.decorate() #针对没有重写定义的方法赋值给wrapClass作为实例变量,本案例中为涉及的为fun2方法
def fun1(self,*a, **k): print("准备调用被装饰类的方法fun1") self.wrappedClass.fun1(*a, **k) print("调用被装饰类的方法fun1完成")
def decorate(self):#针对没有重写定义的方法赋值给wrapClass作为实例变量
for m in dir(self.wrappedClass): if not m.startswith('_')and m!='fun1': fn = getattr(self.wrappedClass, m) if callable(fn): setattr(self, m,fn)
return wrapClass
@decorateFunctionclass wrappedClass: def __init__(self ,*a, **k): print("我是被装饰类的构造方法") self.name = a[0] if a:print("构造方法存在位置参数:",a) if k:print("构造方法存在关键字参数:",k) print("被装饰类构造方法执行完毕")
def fun1(self,*a, **k): print("我是被装饰类的fun1方法") if a:print("fun1存在位置参数:",a) if k:print("fun1存在关键字参数:",k) print("我的实例名字为:",self.name) print("被装饰类fun1方法执行完毕")
def fun2(self,*a, **k): print("我是被装饰类的fun2方法") if a:print("fun2方法存在位置参数:",a) if k:print("fun2存在关键字参数:",k) print("我的实例名字为:",self.name)
复制代码


针对以上被装饰函数装饰的类 wrappedClass,我们执行如下语句:



>>> c1 = wrappedClass('c1',a=1,b=2)我是被装饰类的构造方法构造方法存在位置参数: ('c1',)构造方法存在关键字参数: {'a': 1, 'b': 2}被装饰类构造方法执行完毕
>>> c2 = wrappedClass('c2',a=12,b=22)我是被装饰类的构造方法构造方法存在位置参数: ('c2',)构造方法存在关键字参数: {'a': 12, 'b': 22}被装饰类构造方法执行完毕
>>> c1.fun1()准备调用被装饰类的方法fun1我是被装饰类的fun1方法我的实例名字为: c1被装饰类fun1方法执行完毕调用被装饰类的方法fun1完成
>>> c2.fun2()我是被装饰类的fun2方法我的实例名字为: c2
>>> c1.fun2()我是被装饰类的fun2方法我的实例名字为: c1
>>>
复制代码


可以看到,除了在装饰类中重写的 fun1 方法可以正常执行外,没有重写的方法 fun2 也可以正常执行。

四、函数的类装饰器

除了用函数作为装饰器装饰函数或者装饰类之外,也可以使用类作为函数的装饰器。将类作为函数的装饰器时,需将要装饰的函数作为装饰器类的实例成员,由于装饰后,调用相关方法时实际上调用的是装饰类的实例对象本身,为了确保类的实例对象可以调用,需要给类增加 __call__方法。

案例:


class decorateClass: def __init__(self,fun): self.fun=fun
def __call__(self, *a, **k): print("执行被装饰函数") return self.fun( *a, **k)
@decorateClass def fun( *a, **k): print(f"我是函数fun,带参数:",a,k) print("老猿Python博客文章目录:https://blog.csdn.net/LaoYuanPython/article/details/109160152,敬请关注同名微信公众号")
复制代码


定义后执行相关调用情况如下:



>>> f = fun('funcation1',a=1,b=2)执行被装饰函数我是函数fun,带参数: ('funcation1',) {'a': 1, 'b': 2}老猿Python博客文章目录:https://blog.csdn.net/LaoYuanPython/article/details/109160152,敬请关注同名微信公众号
>>>
复制代码

五、类的类装饰器

前面分别介绍了函数的函数装饰器、类的函数装饰器、函数的类装饰器,但公开资料中未查到是否可以有类的类装饰器,即装饰器和被装饰对象都是类。老猿参考类的函数装饰器、函数的类装饰器最终确认类的类装饰器也是可以支持的。

5.1、实现要点

要实现类的类装饰器,按老猿的研究,类的装饰器类的实现需要遵循如下要点:


  1. 装饰器类必须实现至少两个实例方法,包括__init__和__call__

  2. 在装饰器类的构造方法的参数包括self,wrapedClass,*a,**k,其中 wrapedClass 代表被装饰类,a 代表被装饰类构造方法的位置参数,k 代表被装饰类构造方法的关键字参数。关于位置参数和关键字参数请参考《第五章函数进阶 第5.1节 Python函数的位置参数、关键字参数精讲》;

  3. 在装饰器类的构造方法中定义一个包装类如叫 wrapClass,包装类从装饰器类的构造方法的参数 wrapedClass(即被装饰类)继承,包装类 wrapClass 的构造方法参数为self,*a,**k,相关参数含义同上;

  4. 在包装类的构造方法中调用父类的构造方法,传入参数 a、k;

  5. 在装饰器类的构造方法中用实例变量(例如 self.wrapedClass)保存 wrapClass 类;

  6. 在装饰器类的__call__方法中调用self.wrapedClass(*a,**k)创建被装饰类的一个对象,并返回该对象。


按照以上步骤创建的类装饰器,就可以用于装饰其他类。当然上述方法只是老猿自己研究测试的结论,是否还有其他方法老猿也不肯定。

5.2、类的类装饰器案例


class decorateClass: #装饰器类 def __init__(self,wrapedClass,*a,**k): #wrapedClass代表被装饰类 print("准备执行装饰类初始化")
class wrapClass(wrapedClass): def __init__(self,*a,**k): print(f"初始化被封装类实例开始,位置参数包括:{a}, 关键字参数为{k}") super().__init__(*a,**k) print(f"初始化被封装类实例结束")
self.wrapedClass=wrapClass print("装饰类初始化完成")
def __call__(self, *a, **k): print("被装饰类对象初始化开始") wrapedClassObj = self.wrapedClass(*a,**k) print("被装饰类对象初始化结束") return wrapedClassObj
@decorateClassclass car: def __init__(self,type,weight,cost): print("class car __init__ start...") self.type = type self.weight = weight self.cost = cost self.distance = 0 print("class car __init__ end.")
def driver(self,distance): self.distance += distance print(f"{self.type}已经累计行驶了{self.distance}公里") print("老猿Python博客文章目录:https://blog.csdn.net/LaoYuanPython/article/details/109160152,敬请关注同名微信公众号")
c = car('爱丽舍','1.2吨',8)c.driver(10)c.driver(110)
复制代码


执行以上代码,输出如下:



准备执行装饰类初始化装饰类初始化完成被装饰类对象初始化开始初始化被封装类实例开始,位置参数包括:('爱丽舍', '1.2吨', 8), 关键字参数为{}class car __init__ start...class car __init__ end.初始化被封装类实例结束被装饰类对象初始化结束爱丽舍已经累计行驶了10公里爱丽舍已经累计行驶了120公里老猿Python博客文章目录:https://blog.csdn.net/LaoYuanPython/article/details/109160152,敬请关注同名微信公众号
复制代码


除了上述方法,老猿又找到了一种更简单的方法,具体请参考《类的类装饰器实现思路及案例》。

六、小结

本文详细介绍了 Python 中的四类装饰器,这四类装饰器根据装饰器和被装饰对象的类型分为函数的函数装饰器、类的函数装饰器、函数的类装饰器、类的类装饰器,文中详细介绍了四类装饰器的实现步骤,并提供了对应的实现案例,相关介绍有助于大家全面及详细地理解 Python 的装饰器。

写博不易,敬请支持:

如果阅读本文于您有所获,敬请点赞、评论、收藏,谢谢大家的支持!

关于老猿的付费专栏

  1. 付费专栏《使用PyQt开发图形界面Python应用》专门介绍基于 Python 的 PyQt 图形界面开发基础教程,对应文章目录为《 使用PyQt开发图形界面Python应用专栏目录》;

  2. 付费专栏《moviepy音视频开发专栏 )详细介绍 moviepy 音视频剪辑合成处理的类相关方法及使用相关方法进行相关剪辑合成场景的处理,对应文章目录为《moviepy音视频开发专栏文章目录》;

  3. 付费专栏《OpenCV-Python初学者疑难问题集》为《 OpenCV-Python图形图像处理 》的伴生专栏,是笔者对 OpenCV-Python 图形图像处理学习中遇到的一些问题个人感悟的整合,相关资料基本上都是老猿反复研究的成果,有助于 OpenCV-Python 初学者比较深入地理解 OpenCV,对应文章目录为《 OpenCV-Python初学者疑难问题集专栏目录 》。


前两个专栏都适合有一定 Python 基础但无相关知识的小白读者学习,第三个专栏请大家结合《OpenCV-Python图形图像处理 》的学习使用。


对于缺乏 Python 基础的同仁,可以通过老猿的免费专栏《专栏:Python基础教程目录)从零开始学习 Python。


如果有兴趣也愿意支持老猿的读者,欢迎购买付费专栏。


如对文章内容存在疑问,可在博客评论区留言,或关注:老猿 Python 微信公号发消息咨询。


跟老猿学 Python!

前往老猿Python博文目录

发布于: 2021 年 04 月 23 日阅读数: 44
用户头像

老猿Python

关注

学问无遗力,功夫老始成。 2020.08.21 加入

CSDN 2020年博客之星季军、高级程序员、超50万行C语言项目开发经验 擅长领域:Python语言、PyQt界面程序开发、Moviepy音视频剪辑、OpenCV-Python图像处理、爬虫、5G、区块链、人工智能数学基础

评论

发布
暂无评论
你不一定全知道的四种Python装饰器实现详解