QQ登录

只需一步,快速开始

开启左侧

Python学习第六十九天—类和对象18-数据描述符、非数据描述符、优雅编程

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

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

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

x
本帖最后由 15271953841 于 2024-4-7 09:02 编辑

数据描述符、非数据描述符、优雅编程
描述符被编写成独立的类,然后将描述符的实例对象赋值给类属性,从而可以实现对该属性读取、写入和删除的全方位拦截,看下面代码有没有毛病:
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.
class D:
    def __get__(self,instance,owner):
        print("get~")

        
class C:
    def __init__(self):
        self.x = D()

        
c = C()
c.x
<__main__.D object at 0x00000166024DBB30>
当这个实例对象小c的属性x被访问的时候,描述符是否能成功的拦截,并没有像预期那样打印一个“get~”,而是将描述符这个对象直接给输出了,这是怎么一回事呢?
这是因为描述符只能应用于类属性,而我们这里把它应用于对象属性了,所以是不行的。
改为:
class C:
    x = D()

   
c = C()
c.x
get~
描述符只能应用于类属性,如果直接应用在对象的属性上,魔法就会失灵。
描述符的定义可细分为两类:数据描述符,非数据描述符,主要是根据所实现的魔法方法不同来进行划分的。
如果实现了__set__()和__delete__()两个其中的任意一个方法,那么该描述符就是一个数据描述符。
如果单单只是实现了__get__()方法,该描述符就是一个非数据描述符。
当发生属性访问的时候,其实它们的优先级别是不一样的。
优先级从高到底依次是:数据描述符、实例对象属性(放在对象的dict字典里的属性)、非数据描述符,类属性(类还要考虑到一个继承,根据MRO的查找顺序)
本课时开头的例子class D:只实现了__get__()方法,所以它是一个非数据描述符,刚刚我们看到,非数据描述符的优先级其实是比数据描述符,甚至是实例对象属性更低的,注意:如果我们通过给实例对象的同名属性赋值,那么它就会覆盖掉拦截。
(上节上一段代码)
c.x = "FishC"
c.x           (看它不打印“get~”了,说明描述符拦截已经失效了,它取而代之的是打印我们刚刚赋值这个字符串“FishC”,不过我们仍然使用类名来访问的话,描述符还是可以拦截到的,只是说这个对象小c的x属性被覆盖了。)
'FishC'
C.x
get~
那么作为对比,我们来重新定义一下这个class D:
class D:
    def __get__(self,instance,owner):
        print("get~")
    def __set__(self,instance,value):
        print("set~")

        
class C:
    x = D()

   
c = C()
c.x
get~
c.x = "FishC"       (现在就被拦截了,set~)
set~
c.x               (然后c.x,直接打印了get~,它就覆盖不了)
get~
c.__dict__
{}
上面的赋值操作牙根没有把“FishC”这个字符串写道这个dict字典里面去,因为它被数据描述符给拦截了,直接赋值行不通,我们采用间接方法试一下,就是将这个属性添加到对象的__dict__()字典里面去。
(上接上面代码)
c.__dict__["x"] = "FishC"
c.__dict__
{'x': 'FishC'}       (写进去了)
c.x               (我们试一下访问,仍然被拦截了)
get~
这就是访问优先级的碾压。
你猜这个优先级是定义在哪一个魔法方法的默认实现呢?是__getattribute__(参考第62课时),__getattribute__是管理属性的获取,所以我们刚刚提到的这个优先级其实就是这个魔法方法的默认实现逻辑。现在我们重新来写一下这个方法:
class C:
    x = D()
    def __getattribute__(self,name):
        print("aha~")

        
c = C()
c.x
aha~                (大家看,现在直接就aha~,没有描述符什么事情,就算是数据描述符也不管用了)
c.x = "FishC"
set~              (有点不明白,现在数据描述符进行了拦截,set~)
c.x
aha~
c.__dict__
aha~
c.__dict__["x"] = "FishC"
aha~
Traceback (most recent call last):
  File "<pyshell#22>", line 1, in <module>
    c.__dict__["x"] = "FishC"
TypeError: 'NoneType' object does not support item assignment
至此,我们介绍了描述符有三个魔法方法:__get__(),__set__(),__delete__(),但是描述符还有第四种魔法方法:__set_name__(),__set_name__(self,owner,name)是Python3.6新添的特性,有什么用?
_set_name__ 方法是 Python 3.6 中引入的一种特殊方法,它可以在类属性被赋值时自动调用。这个方法可以用来处理类属性的名称绑定问题,例如将类属性与其所在的类进行绑定。
具体来说,当一个类定义了一个描述符(descriptor)并将其作为类属性时,Python 将在该类定义完成后自动调用描述符的 __set_name__ 方法,并将该类属性的名称作为参数传递给该方法。这样,我们就可以在 __set_name__ 方法中访问该类属性的名称,并将其与描述符进行绑定。
截至目前,我们利用描述符实现的都是对属性的拦截,在实际的开发工作中,我们费了那么大的功夫,通过描述符去拦截一个属性,然后做了一些额外的工作,之后,通常我们还是要去执行相应的访问,赋值和删除等操作,这些都要做到位,打个比方,我们通过描述符拦截了对象的x属性,进行了合法的验证,看看它的值是不是大于18之类的操作,如果符合要求的话,我们仍然是把它写入实例对象的属性中去的,也就是说,我们需要在描述符里面去操作实例对象这个__dict__字典,对不对,这其实是可以做到的,是不是,为什么?因为这instance参数代表的其实就是描述符所拦截的属性所在的实例对象,我们的代码可以这么写:
class D:
    def __init__(self,name):
        self.name = name
    def __get__(self,instance,owner):
        print("get~")
        return instance.__dict__.get(self.name)
    def __set__(self,instance,value):
        print("set~")
        instance.__dict__[self.name] = value

        
class C:
    x = D("x")       (把这个属性名变成字符串,传递进去,给到这个name)

   
c = C()
c.x
get~                 (因为此时属性x中没有东西,所以它并没有返回)
c.__dict__
{}                     (此时是没有东西的)
c.x = 250
set~                  (打印了一个set~,说明set魔法方法已经成功拦截)
c.__dict__
{'x': 250}            (写进去了,通过描述符间接的把这个数据写到对象的__dict__字典里面去了)
c.x
get~
250                 (也成功的去访问到这个对象的__dict__字典里面这个键值对)
但写这样(x = D("x"))的代码,显得很别扭。
所以就有了这第四个描述符方法:__set_name__,它的功能就是让你的代码变得优雅。
class D:
    def __set_name__(self, owner, name):
        self.name = name
        print(f"D.__set_name__(owner={owner}, name={name})")
        
    def __get__(self, instance, owner):
        print(f"get~\nD.__get__(instance={instance}, owner={owner})")
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        print(f"set~\nD.__set__(instance={instance}, value={value})")
        instance.__dict__[self.name] = value

        
class C:
    x = D()

   
D.__set_name__(owner=<class '__main__.C'>, name=x)
c = C()
c.x
get~
D.__get__(instance=<__main__.C object at 0x00000197B3751610>, owner=<class '__main__.C'>)
c.__dict__
{}
c.x = 250
set~
D.__set__(instance=<__main__.C object at 0x00000197B3751610>, value=250)
c.__dict__
{'x': 250}
c.x
get~
D.__get__(instance=<__main__.C object at 0x00000197B3751610>, owner=<class '__main__.C'>)
250

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

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

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

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