描述符必须赋给一个类属性而不是实例属性,否则它将无法工作。
__init__构造函数中的属性赋值也会触发__setattr__方法,这个方法捕获所有属性赋值,即便是类自身之中的那些。
4个访问器技术:
和property一样,描述符也管理一个单个的、指定的属性。
当描述符的instance参数为None的时候,该描述符就知道它正被类名直接访问。
Python 中与属性访问相关的一些魔法方法:
1.getattr(self, name):当默认属性访问抛出 AttributeError 异常(可能是 getattribute 无法找到对应实例的属性而抛出 AttributeError 异常,或者实例本身无法在继承树上找到对应的实例,再或者是描述符对象的 get 对该属性抛出 AttributeError异常导致)时被调用。该方法应当返回一个值或者抛出一个 AttributeError 异常。
注:如果按正常的属性搜索机制找到了属性,getattr 不会被调用。
-
getattribute(self, name):该方法会被无条件调用, 无论属性存不存在。如果同时定义了 getattr 方法,除非 getattribute 显示调用它或者抛出一个 AttributeError 异常,否则不会被调用。该方法应当返回一个值或者抛出一个 AttributeError 异常。为了避免潜在的无限递归调用,方法的实现应当永远返回基类的同名方法调用,比如object.getattribute(self, name)。
-
getattribute(self, name) 被对象的.操作触发;getitem(self, key) 被对象的[]操作触发。
官方定义:python描述符是一个“绑定行为”的对象属性,在描述符协议中,它可以通过方法重写属性的访问。这些方法有 get(), set(), 和__delete__()。如果这些方法中的任何一个被定义在一个对象中,这个对象就是一个描述符。
描述符是实现了特定协议的类,这个协议包括 get、set 和__delete__ 方法。
描述符魔法方法:get(), set(), delete()
方法的原型为:
-
__get__(self, instance, owner)
-
set__(self, instance, value)__
-
__del__(self, instance)
-
当Python解释器发现实例对象的字典中,有与描述符同名的属性时,描述符优先,会覆盖掉实例属性。数据描述符会覆盖掉对象实例属性字典__dict__,非数据描述符属性会被对象实例对应属性覆盖。
描述符分数据描述符与非数据描述符(只定义了 get() 方法,而没有定义 set(), delete() 方法)
当使用实例对象访问属性时,都会调用__getattribute__内建函数,__getattribute__查找属性的优先级如下:
1、类属性
2、数据描述符
3、实例属性
4、非数据描述符
5、getattr()
Python中对象的属性具有 “层次性”,属性在哪个对象上定义,便会出现在哪个对象的__dict__中。
getattribute__是实例对象查找属性或方法的入口。实例对象访问属性或方法时都需要调用到__getattribute,之后才会根据一定的规则在各个__dict__中查找相应的属性值或方法对象,若没有找到则会调用__getattr__
实例对象的__setattr__方法可以定义属性的赋值行为,不管属性是否存在。当属性存在时,它会改变其值;当属性不存在时,它会添加一个对象属性信息到对象的__dict__中,然而这并不改变类的属性。
搜索链(或者优先链)的顺序大概是这样的:数据描述符>实体属性(存储在实体的_dict_中)>非数据描述符。
覆盖性**-**非覆盖性描述符调用优先级:
非覆盖型的获取属性的优先级链是,getattribute->找__dict__->找描述符, 这条链的规则给了"缓存属性"理论支持
有__get__方法的覆盖型的获取属性的优先级链是,getattribute->找描述符.
没有__get__方法的覆盖型的获取属性优先级链是
在没有对属性赋值时是__getattribute__->找描述符(返回描述符对象本身),对属性赋值了之后是__getattribute__->找__dict__->找描述符.
在 Python 中,静态方法staticmethod() 和 类方法classmethod() 以非数据描述符(non-data descriptor)的方式进行实现,因此,对象实例可以覆盖重定义这些方法。
property() 方法以数据描述符(data descriptor)进行实现,因此,对象实例无法重写property行为。
当 数据描述符作为类属性 时,在对象上使用该属性(a.x,x为 data descriptor)时,赋值时会调用数据描述符的__set__方法,取值时会调用数据描述符的__get__方法。对象的属性字典obj.__dict__不起作用。当在 类对象(A.data_desc)上使用数据描述符属性时,由于该属性本身就是类对象所有,因此设置属性时,不会调用数据描述符的__set__方法,而是直接设置到类的__dict__中;而取值流程与对象的流程一致,只是数据描述符__get__的参数instance变为None。
对象访问非数据描述符时,先调用对象的__dict__,没有才调用描述符的__get__方法。对于非数据描述符,类对象使用时直接调用其__get__方法。
vars方法用于查看对象的属性,等价于对象的__dict__内容。
类属性可以使用对象和类访问,多个实例对象共享一个类变量。但是只有类才能修改。
对象中属性访问的默认行为就是在对象的字典中get、set或delete相应的属性。例如,a.x的查找顺序是从 a.dict[‘x’] 到 type(a).dict[‘x’],然后继续在type(a)除元类(metaclass)外的基类中查找。如果要查找的值是定义了任意Descriptor方法的对象,那么Python会调用Descriptor方法来覆盖属性查找的默认行为。查找的优先级顺序取决于定义了哪些Descriptor方法。
Data Descriptor和Non-data Descriptor的不同体现在关于实例字典条目的覆盖和计算顺序上。如果实例字典中包含了与Data Descriptor同名的属性,那么Data Descriptor优先。如果实例字典中包含了与Non-data Descriptor同名的属性,实例字典优先。
调用的细节取决于obj是对象还是类。
对于对象来说,其机制是object.getattribute()将b.x转换为type(b).dict[‘x’].get(b, type(b))。其实现的优先级链是:Data Descriptor优先级高于实例变量(instance variables),实例变量优先级高于Non-data Descriptor,而 getattr() 的优先级是最低的。完整的c代码实现在Objects/object.c的PyObject_GenericGetAttr()函数中。
对于类来说,其机制是type.getattribute()将B.x转换为B.dict[‘x’].get(None, B)。
需要记住的重要几点:
Descriptor是通过__getattribute__()方法来调用的
覆写__getattribute__()可以阻止Descriptor的自动调用
object.getattribute()和type.getattribute()调用__get__()的方式不同
Data Descriptor总是覆盖实例字典
Non-data Descriptor可能会被实例字典覆盖
为了支持方法调用,函数有__get__()方法,可以在属性访问时绑定方法。这意味着所有的函数都是Non-data Descriptor,根据调用方是对象或类来返回绑定或非绑定方法。
静态方法返回没有任何变化的原函数。调用c.f或C.f相当于直接查找object.getattribute(c, “f”)或object.getattribute(C, “f”)。因此,函数通过对象或类来调用是等价的。
当函数仅需要类引用并且不关心任何内部数据时,类方法是非常有用的。类方法的一个用途就是代替类构造函数来创建对象。
如前所述,Python 存取属性的方式特别不对等。通过实例读取属性时,通常返回的是实例中定义的属性;但是,如果实例中没有指定的属性,那么会获取类属性。而为实例中的属性赋值时,通常会在实例中创建属性,根本不影响类。
实现 set 方法的描述符属于覆盖型描述符,因为虽然描述符是类属性,但是实现 set 方法的话,会覆盖对实例属性的赋值操作。
通常,覆盖型描述符既会实现 set 方法,也会实现 get 方法,不过也可以只实现 set 方法。此时,只有写操作由描述符处理。通过实例读取描述符会返回描述符对象本身,因为没有处理读操作的 get 方法。如果直接通过实例的 __dict__属性创建同名实例属性,以后再设置那个属性时,仍会由 set 方法插手接管,但是读取那个属性的话,就会直接从实例中返回新赋予的值,而不会返回描述符对象。也就是说,实例属性会遮盖描述符,不过只有读操作是如此。
不管描述符是不是覆盖型,为类属性赋值都能覆盖描述符。