python中描述符(descriptor类)详解

1、什么是描述符?

  python描述符是一个“绑定行为”的对象属性,在描述符协议中,它可以通过方法重写属性的访问。这些方法有 __get__(), __set__(), 和__delete__()。如果这些方法中的任何一个被定义在一个对象中,这个对象就是一个描述符。

  以上为官方定义,纯粹为了装逼使用,一般人看这些定义都有一种问候祖先的冲动!!!!(WQNMLGB)

2、讲解描述符前,先看一下魔法方法:__dict__ (每个对象均具备该方法)(对象包括类对象,实例对象~~)

  作用:字典类型,存放本对象的属性,key(键)即为属性名,value(值)即为属性的值,形式为{attr_key : attr_value}

  对象属性的访问顺序:

  ①.实例属性

  ②.类属性

  ③.父类属性

  ④.__getattr__()方法

  以上顺序,切记切记!!!

 例子:

class Test(object):     #object是所有类的基类!!
    cls_val = 1       #类属性 class_val 
    def __init__(self):
        self.ins_val = 10  #实例属性  instance_val

        
>>> t=Test()
>>> Test.__dict__
mappingproxy({'__module__': '__main__', 'cls_val': 1, '__init__': <function Test.__init__ at 0x0000000002E35D08>, '__dict__': <attribute '__dict__' of 'Test' objects>, '__weakref__': <attribute '__weakref__' of 'Test' objects>, '__doc__': None})
>>> t.__dict__
{'ins_val': 10}
#更改实例t的属性cls_val,只是为对象新增了该 实例属性,并不影响类Test的 类属性cls_val
>>> t.cls_val = 20
>>> t.__dict__
{'ins_val': 10, 'cls_val': 20}
>>> Test.__dict__
mappingproxy({'__module__': '__main__', 'cls_val': 1, '__init__': <function Test.__init__ at 0x0000000002E35D08>, '__dict__': <attribute '__dict__' of 'Test' objects>, '__weakref__': <attribute '__weakref__' of 'Test' objects>, '__doc__': None})

#更改了类Test的属性cls_val的值,由于事先增加了实例t的cls_val属性,因此也不会改变实例的cls_val值(井水不犯河水) >>> Test.cls_val = 30 >>> t.__dict__ {'ins_val': 10, 'cls_val': 20} >>> Test.__dict__ mappingproxy({'__module__': '__main__', 'cls_val': 30, '__init__': <function Test.__init__ at 0x0000000002E35D08>, '__dict__': <attribute '__dict__' of 'Test' objects>, '__w
eakref__
': <attribute '__weakref__' of 'Test' objects>, '__doc__': None})

从以上代码可知,类属性和实例属性不一样!虽然可以同名,但代表的是不同的两个属性!!

 3、魔法方法:__get__(), __set__(), __delete__()

  方法的原型为:

  ① __get__(self, instance, owner)

  ② __set__(self, instance, value)

  ③ __del__(self, instance)

  那么以上的 self, instance owner到底指啥么呢?

  首先我们先看一段代码:

#代码 1

class Desc(object):    
    
    def __get__(self, instance, owner):
        print("__get__...")
        print("self : 		", self)
        print("instance : 	", instance)
        print("owner : 	", owner)
        print('='*40, "
")
        
    def __set__(self, instance, value):
        print('__set__...')
        print("self : 		", self)
        print("instance : 	", instance)
        print("value : 	", value)
        print('='*40, "
")


class TestDesc(object):
    x = Desc()    #x是Desc类的实例对象 ,同时x也是TestDesc的类属性 ;    Desc()实例化一个对象,并用x对其引用,所以x就是Desc类的实例化对象

#以下为测试代码 t = TestDesc()    #t是TestDesc类的实例对象 t.x #以下为输出信息: __get__... self : <__main__.Desc object at 0x0000000002B0B828> instance : <__main__.TestDesc object at 0x0000000002B0BA20> owner : <class '__main__.TestDesc'> ========================================

由输出信息可以知道:

  ① self:   Desc的实例对象,其实就是TestDesc的类属性x

  ② instance: TestDesc的实例对象,其实就是t

  ③ owner:  即谁拥有这些东西,当然是 TestDesc这个类;它是最高统治者,其他的一些都是包含在它的内部或者由它生出来的

其实,Desc类就是是一个描述符(描述符是一个类哦),为啥呢?因为类Desc定义了方法 __get__, __set__. __delete__

所以,某个类,只要是内部定义了方法 __get__, __set__, __delete__ 中的一个或多个,就可以称为描述符(^_^,简单吧)

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

到这儿,还是有很多问题的!

问题1. 为什么访问 t.x的时候,会直接去调用描述符的 __get__() 方法呢?

    答:t为实例,访问t.x时,根据常规顺序,

      首先:访问Owner 即 TestDesc的__getattribute__()方法(其实就是 TestDesc.__getattribute__()) ,访问实例属性 ; 发现没有 , 然后去类TestDesc中访问,找到了类属性x

      其次:判断类属性 x 为一个描述符,此时,它就会做一些变动了,将 TestDesc.x 转化为   TestDesc. __dict__['x'].__get__(None, TestDesc)  来访问

      然后:进入类Desc 的 __get__()方法,进行相应的操作

问题2. 从上面 代码1 我们看到了,描述符的对象 x 其实是类 TestDesc  的类属性,那么可不可以把它变成实例属性呢? 

    答:看看解释器怎么说的。

#代码 2

class Desc(object):
    def __init__(self, name):
        self.name = name
    
    def __get__(self, instance, owner):
        print("__get__...")
        print('name = ',self.name) 
        print('='*40, "
")

class TestDesc(object):
    x = Desc('x')
    def __init__(self):
        self.y = Desc('y')      #把描述符(Desc类)的对象变成实例属性

#以下为测试代码
t = TestDesc()
t.x
t.y

#以下为输出结果:
__get__...
name =  x
========================================
<__main__.Desc object at 0x03FB0088>      #没有打印t.y的信息,但至少证明了y是Desc类的实例对象

  为什么没有打印t.y的信息呢?

  因为没有访问 __get__() 方法啊,那么为啥没有访问 __get__() 方法呢?

  因为访问 t.y 时刻,首先会调用TestDesc(即Owner)的 __getattribute__() 方法(就是 TestDesc.__getattribute__())  ,先来访问实例属性,找到 y ,又发现属性y 为一个描述符,于是将t.y

  转化成TestDesc.__dict__['y'].__get__(t, TestDesc)  ;但是,实际上 TestDesc 并没有 y这个属性,y 是属于实例对象的,所以,只能忽略了。

  》》访问实例层次上的描述符 x,只会返回描述符本身。为了让描述符能够正常工作,它们必须定义在类的层次上。如果不这么做,那么 Python 无法自动为你调用 __get__ 和 __set__ 方法

 问题3. 如果   类属性的描述符对象  和   实例属性描述符的对象  同名时,咋整?

#代码 3

class Desc(object):
    def __init__(self, name):
        self.name = name
        print("__init__(): name = ",self.name)
        
    def __get__(self, instance, owner):
        print("__get__() ...")
        return self.name

    def __set__(self, instance, value):
        self.value = value
        
class TestDesc(object):
    _x = Desc('x')
    def __init__(self, x):
        self._x = x


#以下为测试代码
t = TestDesc(10)
t._x

#输入结果
__init__(): name =  x    #此语句什么也不输入也会打印! 原因 TestDesc类中有一个Desc类实例化并初始化的动作,调用了__init__
__get__() ...
'x'

  根据对象属性的访问顺序,t._x 应该先去调用 __getattribute__() 方法,然后找到了 实例t 的 _x 属性就结束了,为啥还去调用了描述符的 __get__() 方法呢???

  这就牵扯到了一个查找顺序问题当Python解释器发现实例对象的字典中,有与  描述符同名的属性时, 描述符优先,会覆盖掉实例属性

  验证一下:用__dict__查看一下属性

>>> t.__dict__
{}

>>> TestDesc.__dict__
mappingproxy({'__module__': '__main__', '_x': <__main__.Desc object at 0x0000000002B0BA20>, '__init__': <function TestDesc.__init__ at 0x0000000002BC59D8>, '__dict__': <attribute '__dict__' of 'TestDesc' objects>, '__weakref__': <attribute '__weakref__' of 'TestDesc' objects>, '__doc__': None})

  

  我们再将 代码3 改进一下, 删除 __set__() 方法试试看会发生什么情况?

#代码 4

class Desc(object):
    def __init__(self, name):
        self.name = name
        print("__init__(): name = ",self.name)
        
    def __get__(self, instance, owner):
        print("__get__() ...")
        return self.name
        
class TestDesc(object): _x = Desc('x') def __init__(self, x): self._x = x #以下为测试代码 t = TestDesc(10) t._x #以下为输出: __init__(): name = x    

  !? 怎么没有调用__get__方法?

  其实,还是 属性查找优先级 惹的祸,只是定义一个 __get__() 方法,为非数据描述符,优先级低于实例属性的!!

问题4. 什么是数据描述符,什么是非数据描述符? 

  答:一个类,如果只定义了 __get__() 方法,而没有定义 __set__(), __delete__() 方法,则认为是非数据描述符; 反之,则成为数据描述符

问题5. 属性查询优先级

  

    ① __getattribute__(), 无条件调用

    ② 数据描述符:由 ① 触发调用 (若人为的重载了该 __getattribute__() 方法,可能会调职无法调用描述符)

    ③ 实例对象的字典(若与描述符对象同名,会被覆盖哦)

    ④ 类的字典

    ⑤ 非数据描述符

    ⑥ 父类的字典

    ⑦ __getattr__() 方法

参考原文  https://www.cnblogs.com/Jimmy1988/p/6808237.html  

 

原文地址:https://www.cnblogs.com/hemengjita/p/12292548.html