先说定义, 这里直接翻译官方英文文档:
一般来说, 描述符是具有 "绑定行为" 的对象属性, 该对象的属性访问将会被描述符协议中的方法覆盖. 这些方法是__get__(),__set__(), 和__delete__(). 如果一个对象定义了这些方法中的任何一个, 它就是一个描述符.
接下来对这个定义进行解释:
我们访问一个对象 a 的属性 x 的时候, 是这么调用的: a.x, 那么这种方便的调用方式其实是怎么工作的呢?
首先, 它会访问自己的实例名称空间:
a.__dict__['x']
如果没有, 则会访问类及超类的名称空间, 大致上是这个意思:
- for cls in type(a).__mro__:
- if hasattr(cls, 'x'):
- return cls.__dict__['x']
但如果该属性绑定了一个带有__get__的类的实例化对象, 这个时候, b.x 的工作方式就与上面不同了:
- class Desc:
- val = 1
- def __get__(self, instance, owner):
- return self.val
- class B:
- x = Desc()
- b = B()
- # b.x 调用路径: type(b).__dict__['x'].__get__(b, type(b))
- # 需要注意的一点是, 定义了描述符之后, 在构造方法里为同名变量赋值是无效的
- print(b.x)
- >>>1
这是怎么实现的呢? 要解释清楚这个原理, 要先说明一下__getattribute__函数, 当我们调用一个属性的时候, 底层其实就是在执行该函数, 该函数的工作方式是:
B.x => B.__dict__['x'] => 如果 存在__get__方法 则 B.__dict__['x'].__get__(None, B)
具体代码如下:
- def __getattribute__(self, key):
- "Emulate type_getattro() in Objects/typeobject.c"
- v = object.__getattribute__(self, key)
- if hasattr(v, '__get__'):
- return v.__get__(None, self)
- return v
所以, 我们给 B.x 绑定改的是一个对象, 返回的却是该对象的__get__方法的返回值, 重写这个函数, 我们就可以停止描述符的调用.
接下来再解释__set__:
- class Desc:
- num = 1
- def __get__(self, instance, owner):
- return self.num
- def __set__(self, instance, value):
- self.num += value
- class B:
- x = Desc()
- b = B()
- b.x = 2
- print(b.x)
- >>>3
我们给 b.x 赋值为 2, 结果输出的 b.x 则为 3, 神奇吗?
这个概念可能不太好理解, 其原因是这里的'='符号被重载了, 不再是赋值的意思.
如果 B.__dict__['x'] 中没有__set__方法,'='符号则执行其父类的__set__, 一般来说, 就是正常的赋值.
如果 B.__dict__['x'] 重写了__set__方法,'='符号则执行该重写的方法, 即 B.__dict__['x'].__set__(None, value)
利用这一特性, 我们可以在 python 程序中创建常量, 只需要在__set__方法里抛出一个异常即可.
至于 __delete__, 在 del b.x 时会触发, 如果未定义, 则报错
ps: Properties, bound methods, static methods, class methods 都是描述符协议的应用. 欲知后事如何, 请看英文文档: https://docs.python.org/3/howto/descriptor.html
来源: https://www.cnblogs.com/lorthevan/p/10502255.html