python学习交流

Python 之描述子

2020-03-25  本文已影响0人  水之心

参考资料:python3.8-howto/descriptor.

一般地,一个描述器(descriptor)是一个包含 “绑定行为” 的对象,对其属性的存取被描述器协议中定义的方法覆盖。这些方法有:__get__()__set__()__delete__()。如果某个对象中定义了这些方法中的任意一个,那么这个对象就可以被称为一个描述器。

如果一个对象定义了 __set__() or __delete__(),它将被视为一个数据描述子(data descriptor)。如果仅仅定义了 __get__() 该对象被称为非数据描述子(non-data descriptors)。

属性访问的默认行为是从一个对象的字典中获取、设置或删除属性。例如,a.x 的查找顺序会从 a.__dict__['x'] 开始,然后是 type(a).__dict__['x'],接下来依次查找 type(a) 的基类,不包括元类。 如果找到的值是定义了某个描述器方法的对象,则 Python 可能会重载默认行为并转而发起调用描述器方法。具体发生在优先级链的哪个环节则要根据所定义的描述器方法及其被调用的方式来决定。

Descriptors simplify the underlying C-code and offer a flexible set of new tools for everyday Python programs.

描述器协议

descr.__get__(self, obj, type=None) -> value

descr.__set__(self, obj, value) -> None

descr.__delete__(self, obj) -> None

To make a read-only data descriptor, define both __get__() and __set__() with the __set__() raising an AttributeError when called. Defining the __set__() method with an exception raising placeholder is enough to make it a data descriptor.

如果 data descriptor 定义了 __get__() 方法 __set__()__set__() 的回调会引发 AttributeError 错误,则其被称为 read-only

Invoke 描述符

一般地,描述符均有 __get__ 方法,用于获取属性值,比如:d.__get__(obj)。下面主要介绍属性拦截器:

  1. object.__getattribute__(self, name) 方法会无条件地被调用以实现对类实例属性的访问。如果类还定义了 __getattr__(),则后者不会被调用,除非 __getattribute__() 显式地调用它或是引发了 AttributeError。此方法应当返回(找到的)属性值或是引发一个 AttributeError 异常。为了避免此方法中的无限递归,其实现应该总是调用具有相同名称的基类方法来访问它所需要的任何属性,例如 object.__getattribute__(self, name)
  2. object.__getattr__(self, name) 当默认属性访问因引发 AttributeError 而失败时被调用 (可能是调用 __getattribute__() 时由于 name 不是一个实例属性或 self 的类关系树中的属性而引发了 AttributeError;或者是对 name 特性属性调用 __get__() 时引发了 AttributeError)。此方法应当返回(找到的)属性值或是引发一个 AttributeError 异常。

请注意如果属性是通过正常机制找到的,__getattr__() 就不会被调用。(这是在 __getattr__()__setattr__() 之间故意设置的不对称性。)这既是出于效率理由也是因为不这样设置的话 __getattr__() 将无法访问实例的其他属性。要注意至少对于实例变量来说,你不必在实例属性字典中插入任何值(而是通过插入到其他对象)就可以模拟对它的完全控制。

实现细节:

  1. 对于 objects,使用 object.__getattribute__()b.x 转换为 type(b).__dict__['x'].__get__(b, type(b))。该实现的优先级:data descriptors > instance variables > non-data descriptors > __getattr__() 对象。
  2. 对于 classes,使用 type.__getattribute__()B.x 转换为 B.__dict__['x'].__get__(None, B)

划重点:

一个实例

下面直接看一个实例:

class Person:
    def __init__(self):
        self.name = '晓丽'
        self.age = 27
        self.gender = '女'
        
    def __getattribute__(self, item):
        if item == 'age':
            return "问年龄是不礼貌的行为"
        else:
            return object.__getattribute__(self, item)
        
        
if __name__ =='__main__':
    a = Person()
    print(a.name, a.gender)
    print(a.age)

输出:

晓丽 女
问年龄是不礼貌的行为

在实例化的对象中进行 . 运算(包括属性和方法的访问,即:a.xxx 或者 a.xxx()),都会调用 __getattribute__ 方法。但是,如果某个属性通过 __getattribute__ 方法找不到,则会调用 __getattr__ 方法。

比如:

class A(Person):
    def __init__(self):
        super().__init__()
        
    def __getattr__(self, item):
        # 会再次访问 __getattribute__
        return eval("self."+item.lower())
    
if __name__ =='__main__':
    a = A()
    print(a.name, a.Name)
    print(a.age) 

输出:

晓丽 晓丽
问年龄是不礼貌的行为

该例子输出了不存在的属性 Name,这是由于 __getattr__ 放宽了属性的命名。

__dict__ 的访问

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

对象属性的访问顺序:

  1. 实例属性
  2. 类属性
  3. 父类属性
  4. __getattr__() 方法

以上顺序,切记切记!

比如:

class Test(object):
    cls_val = 1
    def __init__(self):
        self.ins_val = 10

t = Test()
Test.__dict__

输出:

mappingproxy({'__module__': '__main__',
              'cls_val': 1,
              '__init__': <function __main__.Test.__init__(self)>,
              '__dict__': <attribute '__dict__' of 'Test' objects>,
              '__weakref__': <attribute '__weakref__' of 'Test' objects>,
              '__doc__': None})

可以查看:

t.__dict__

输出实例属性字典:

{'ins_val': 10}

也可以查看实例的类型:

type(t) == Test

输出为 True。更改实例 t 的属性 cls_val,只是新增了该属性,并不影响类 Test 的属性 cls_val

>>> t.cls_val = 20
>>> t.__dict__

输出结果为:

{'ins_val': 10, 'cls_val': 20}

Test.cls_val == 1 仍然为 True。反之,更改了类 Test 的属性 cls_val 的值,并不会改变实例的 cls_val 值。

从以上代码可以看出,实例 t 的属性并不包含 cls_valcls_val 是属于类 Test 的。

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

通常情况下,我们在访问类或者实例对象的时候,会牵扯到一些属性访问的魔法方法,主要包括:

  1. __getattr__(self, name): 访问不存在的属性时调用
  2. __getattribute__(self, name):访问存在的属性时调用(先调用该方法,查看是否存在该属性,若不存在,接着去调用 __getattr__(self, name)
  3. __setattr__(self, name, value):设置实例对象的一个新的属性时调用
  4. __delattr__(self, name):删除一个实例对象的属性时调用

实例对象属性寻找的顺序如下:

① 首先访问 __getattribute__() 魔法方法(隐含默认调用,无论何种情况,均会调用此方法)
② 去实例对象t中查找是否具备该属性: t.__dict__ 中查找,每个类和实例对象都有一个 __dict__ 的属性
③ 若在 t.__dict__ 中找不到对应的属性, 则去该实例的类中寻找,即 t.__class__.__dict__
④ 若在实例的类中也招不到该属性,则去父类中寻找,即 t.__class__.__bases__.__dict__ 中寻找
⑤ 若以上均无法找到,则会调用 __getattr__ 方法,执行内部的命令(若未重载 __getattr__ 方法,则直接报错:AttributeError)

以上几个流程,即完成了属性的寻找。

看实例:

class Test:
    def __getattr__(self, name):
        print('__getattr__')

    def __getattribute__(self, name):
        print('__getattribute__')

    def __setattr__(self, name, value):
        print('__setattr__')

    def __delattr__(self, name):
        print('__delattr__')

t = Test()
t.x

输出内容为:

__getattribute__

但是,为什么没有调用 __getattr__ 呢?因为,一旦重载了 __getattribute__() 方法,如果找不到属性,则必须要手动加入第④步,否则无法进入到 第⑤步 (__getattr__)的。

验证一下以上说法是否正确:

class Test:
    def __getattr__(self, name):
        print('__getattr__')

    def __getattribute__(self, name):
        print('__getattribute__')
        object.__getattribute__(self, name) # or super().__getattribute__(name)

    def __setattr__(self, name, value):
        print('__setattr__')

    def __delattr__(self, name):
        print('__delattr__')

t = Test()
t.x

输出内容为:

__getattribute__
__getattr__

此时,符合我们的预期了。

下面我们设定实例属性:

t.x = 10

输出:

__setattr__

同样,可以查看删除操作:

del t.x

输出:

__delattr__

下面看看如何使用 __get__(), __set__(), __delete__() 来操作数据。方法的原型为:

__get__(self, instance, owner)
__set__(self, instance, value)
__del__(self, instance)

直接看例子:

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

class TestDesc:
    x = Desc()

t = TestDesc()
t.x

输出:

__get__...
self:    <__main__.Desc object at 0x7fe3858a0950>
instance :   <__main__.TestDesc object at 0x7fe3858a0410>
owner:   <class '__main__.TestDesc'>
======================================== 

可以看到,实例化类 TestDesc 后,调用对象 t 访问其属性 x,会自动调用类 Descget方法,由输出信息可以看出:

  1. self: Desc的实例对象,其实就是 TestDesc 的属性 x
  2. instance: TestDesc的实例对象,其实就是 t
  3. owner: 即谁拥有这些东西,当然是 TestDesc 这个类,它是最高统治者,其他的一些都是包含在它的内部或者由它生出来的
class Desc:
    def __init__(self, name):
        self.name = name
    
    def __get__(self, instance, owner):
        print("__get__...")
        print('name = ',self.name) 
        print('='*40, "\n")

class TestDesc:
    x = Desc('x')
    def __init__(self):
        self.y = Desc('y')

# 测试

因为调用 t.y 时,首先会去调用TestDesc(即Owner)的 __getattribute__() 方法,该方法将 t.y 转化为TestDesc.__dict__['y'].__get__(t, TestDesc), 但是呢,实际上 TestDesc 并没有 y 这个属性,y 是属于实例对象的,所以,只能忽略了。故而输出为:

__get__...
name =  x
======================================== 

<__main__.Desc at 0x7fe3840963d0>

总结一下属性查询优先级:

  1. __getattribute__(), 无条件调用
  2. 数据描述符:由 __getattribute__() 触发调用 (若人为的重载了该 __getattribute__() 方法,可能会无法调用描述符)
  3. 实例对象的字典(若与描述符对象同名,会被覆盖哦)
  4. 类的字典
  5. 非数据描述符
  6. 父类的字典
  7. __getattr__() 方法
上一篇下一篇

猜你喜欢

热点阅读