QQ登录

只需一步,快速开始

开启左侧

Python学习第七十一天—类和对象20-类装饰器

[复制链接]
15271953841 发表于 2024-4-13 09:38:05 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?注册

x
类装饰器
装饰器,它可以拦截函数的调用,并美其名曰——这是在装饰。那么装饰器其实也可以作用到类上面,如果将装饰器作用到类上面,那么理所应当,作用效果应该是这个样子:
Python 3.12.1 (tags/v3.12.1:2305ca5, Dec  7 2023, 22:03:25) [MSC v.1937 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license()" for more information.
>>>def report(cls):
    def oncall(*args,**kwargs):
        print("主人,我要开始实例化对象了……")
        _ = cls(*args,**kwargs)
        print("主人,实例化完成啦,快夸夸我^O^")
        return _
    return oncall

>>>@report
class C:
    pass

>>>c = C()
主人,我要开始实例化对象了……
主人,实例化完成啦,快夸夸我^O^
>>>C
<function report.<locals>.oncall at 0x00000207C1FBE8E0>
这个装饰器与之前学过的装饰到函数上的装饰器,其实用法是非常相似的(参考第40课时),有人会问:*args,**kwargs有什么用呢?这个类目前不是啥都没有吗(只有一个pass),如果这个类里面存在构造函数,它们就有用了。
>>>@report
class C:
    def __init__(self,x,y,z):
        self.x = x
        self.y = y
        self.z = z
        print("构造函数被调用了~")

        
>>>c = C(1,2,3)
主人,我要开始实例化对象了……
构造函数被调用了~
主人,实例化完成啦,快夸夸我^O^
>>>C
<function report.<locals>.oncall at 0x00000207C1FBE980>
首先这个@report是个语法糖,相当于C = report(C),于是这里C(1,2,3)相当于oncall(1,2,3)去调用oncall函数,然后把这个参数(1,2,3)给传递进去,然后把目光转移到oncall函数中去,由于形参是收集参数,(1,2,3)被打包成元组的形式存放到args这个参数里面去了,然后打印一句“主人,我要开始实例化对象了……”,接着,调用闭包(参考第39课时)函数外层传递进来的cls参数,此时才是真正实例化对象的时刻,那么刚刚的args是收集参数,它拿到的一个打包的元组args—>(1,2,3),将多个元素打包成一个元组,这叫收集,我们不能直接将元组作为参数给进行实例化对象,因为这个class C:构造函数需要的是分别给到x,y,z三个形参,我们还需要在这里加个*号对元组进行解包(参考第37课时)cls(*args),那么,实例化对象调用类的构造函数,打印一句“构造函数被调用了~”,然后返回实例化对象,这里我们还在oncall这个函数体内,我们要想办法把这个类C的实例化对象给传递出去,给回到“小c”,所以我们动用到了临时变量“_”,所以我们通过这个return语句把临时变量给返回,“小c”成功拿到这个类C的实例化对象。
类装饰器的作用就是在类被实例化对象之前对其进行拦截和干预,那么这个例子我们演示的是类装饰器在类上面的实现效果,那么反过来,我们能不能让类来做装饰器,然后,来用它来装饰函数呢:其实也是可以的。
>>>class Counter:
    def __init__(self):
        self.count = 0
    def __call__(self,*args,**kwargs):        (当对象被当作函数样子加个小括号去调用的时候,那么它就会访问__call__这个魔法方法(参考第65课时))
        self.count += 1
        print(f"已经被调用了{self.count}次~")

        
>>>c = Counter()
>>>c()
已经被调用了1次~
>>>c()
已经被调用了2次~
>>>c()
已经被调用了3次~
现在这个类Counter的作用就是统计对象被调用了多少次,因为当对象被当作函数去调用的时候,就会触发__call__魔法方法,现在我们想把这个类当作装饰器来使用,计划是用它来装饰指定的函数,我们用一个类作为装饰器去装饰一个函数,然后统计函数被调用的此数,改造如下:
>>>class Counter:
    def __init__(self,func):                (这里我们要让它(func)去修改一个函数,那么我们应该在装饰器生效的时候,把这个函数给获取,装饰器生效,就是实例化一个对象,现在这个装饰器是一个类,self.func = func,传递到self.func这个属性里面去。)
        self.count = 0
        self.func = func
    def __call__(self,*args,**kwargs):
        self.count += 1
        print(f"已经被调用了{self.count}次~")
        return self.func(*args,**kwargs)        (然后在__call__假装被调用的时候,return一个self.func,把*args,**kwargs这个两个参数传递进去,这两个参数的用法,前面讲了)

   
>>>@Counter               (这里我们要将类Counter做装饰器来使用,所以@Counter相当于say_hi=Counter(say_hi),然后把say_hi传进去,那么这个装饰器就相当于实例化对象,并将对象的名字say_hi,同时将say_hi这个函数传递给Counter的构造函数,所以返回的这个say_hi=Counter(say_hi)虽然跟函数的名字一样,其实它早就物是人非了,它已经被调包成立Counter的实例化对象)
def say_hi():
    print("嗨~")

   
>>>say_hi                    (我们在调用say_hi的时候,其实是将Counter的对象当作函数来调用,从而触发了这个__call__魔法方法)
<__main__.Counter object at 0x00000207C1FC1D60>
>>>say_hi()
已经被调用了1次~
嗨~
>>>say_hi()
已经被调用了2次~
嗨~
>>>say_hi()
已经被调用了3次~
嗨~
>>>Counter(say_hi)
<__main__.Counter object at 0x00000207C1FC1670>

下面用一段代码来考核一下大家对装饰器的理解:
>>>class Check:
    def __init__(self,cls):
        self.cls = cls
    def __call__(self,*args,**kwargs):
        self.obj = self.cls(*args,**kwargs)
        return self
    def __getattr__(self,name):
        print(f"正在访问{name}…")
        return getattr(self.obj,name)

   
>>>@Check
class C:
    def say_hi(self):
        print("嗨~")
    def say_hey(self):
        print("嘿~")

        
>>>c = C()
>>>c.say_hi()
正在访问say_hi…
嗨~
>>>c.say_hey()
正在访问say_hey…
嘿~
对于这个测试用的class C:似乎是没有问题,如果上面的代码没有问题,如果class这么改写:
>>>@Check
class C:
    def __init__(self,name):
        self.name = name
    def say_hi(self):
        print("嗨~")
    def say_hey(self):
        print("嘿~")

>>>c1 = C("c1")
>>>c2 = C("c2")
>>>c1.name
正在访问name…
'c2'
>>>c2.name
正在访问name…
'c2'
>>>c1.say_hi()
正在访问say_hi…
嗨~
>>>c2.say_hi()
正在访问say_hi…
嗨~
显然c1的name属性被c2的name属性覆盖了,原因:首先这个类C被@Check这个装饰器处理过了,C=Check(C),于是这个c1和c2看着纵然像类C的实例对象,但其实它不是,它是Check类的对象。
>>>c1
<__main__.Check object at 0x00000207C1FC04D0>
>>>c2
<__main__.Check object at 0x00000207C1FC04D0>
所以c1和c2这两个字符串参数其实是传递了Check类的__call__()魔法方法,我们这里(c1=C(“c1”))看作像在实例化对象,其实并不是。其实是调用早已实例好的对象,因为对象是在@Check的时候C=Check(C)就诞生了的,那么这个class C:反而是在__call__()魔法方法中第一个语句完成实例化,并将实例化对象传递给了self.obj属性,但是它并不是返回self.obj,它返回的是self,也就是Check类的对象自身,而非类C的对象,于是我们在访问c1.name和c2.name的时候,实际上是访问的是Check类实例对象的name属性,那Check类也没有name属性呀,它就会去试图查找__getattr__()这个魔法方法,这个__getattr__()魔法方法就是当我们去访问一个对象并没有存在的属性才会触发的,恰好这个魔法方法我们又有了定义,它的执行逻辑是先打印一段字符串,然后调用getattr函数,也就是获取self.obj的name属性(参考第62课时),那self.obj保存的是什么,它保存的是类C的实例对象,所以搞了一圈,又回来了,那么这里为什么会触发name属性的覆盖bug呢?我们这里要问,这个Check类到底实例化了多少个对象?一个而已吗,是在做装饰器的时候,C=Check(C)仅仅被实例化了一次而已,那么这两个c1=C(“c1”),c2=C(“c2”)其实只不过是把对象当函数调用了两次,从而访问了两次相应的__call__()魔法方法,由于调用了两次__call__()魔法方法,所以self.obj第一次保存的是name为c1的类C实例化对象,而第二次调用的则被name为c2的类C实例化对象所覆盖,好,发现bug的来源了,如何解决呢?有bug,我们就补bug,其实解决的方法也是很巧妙,就是在这个装饰器外面再套上一层:
>>>def report(cls):                (在外面套一层函数叫report,在这里把cls给传递进去)
    class Check:
        def __init__(self,*args,**kwargs):      (cls传递进来,这里就拿到了)
            self.obj = cls(*args,**kwargs)    (这里进行了实例化把两个收集参数传递进去)
        #def __call__(self,*args,**kwargs):         (不要__call__()了)
            #self.obj = self.cls(*args,**kwargs)
            #return self
        def __getattr__(self,name):
            print(f"正在访问{name}…")
            return getattr(self.obj,name)
    return Check                          (return Check类)

>>>@report            (这里Check改成report)
class C:
    def __init__(self,name):
        self.name = name
    def say_hi(self):
        print(f"嗨{self.name}~")
    def say_hey(self):
        print("嘿{self.name}~")

        
>>>c1 = C("c1")
>>>c2 = C("c2")
>>>c1.name
正在访问name…
'c1'
>>>c2.name
正在访问name…
'c2'
>>>c1.say_hi()
正在访问say_hi…
嗨c1~
>>>c2.say_hey()
正在访问say_hey…
嘿{self.name}~
bug被解决了,被@report装饰过的class C:其实被替换成了class Check,因为这个report返回的是这个Check类,那么当执行c1=C(“c1”),c2=C(“c2”)这个两句的时候,就相当于在实例化Check类,实例化Check类,就会调用它的构造函数,我们来看它的构造函数都做了什么事情,原来Check类的构造函数做的事情就是去实例化report装饰器装饰过的这个类,这个class C,让后把实例化对象保存在self.obj属性中,那么这个测试为什么没有出现覆盖name属性的bug呢?因为Check类总共被实例化了两次,而非一次,只是给它套了一次外壳,问题就解决了。巧妙!

客服热线
400-1234-888 周一至周日:09:00 - 21:00
公司地址:襄阳市樊城区长虹路现代城5号楼188

创客帮MAKER.BAND青少年创客创意社区是一个融教育、科技、体育资讯为一体的综合服务平台,专注于教育创新、专注于科技体育、专注于教育资讯。

Powered by Discuz! X3.4 © 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表