先说定义,这里直接翻译官方英文文档:

  一般来说,描述符是具有“绑定行为”的对象属性,该对象的属性访问将会被描述符协议中的方法覆盖.这些方法是__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

03-10 00:34