描述符是对多个属性运用相同存取逻辑的一种方式。它是实现了特定协议的类,property 类就完整地实现了描述符协议。

20.1 描述符示例:验证属性

简单的描述符实现

  • 描述符相关的定义:
    • 描述符类:实现描述符协议的类;
    • 托管类:将描述符实例声明为类属性的类;
    • 储存属性托管实例中存储自身托管属性的属性(实际的实例属性);
    • 托管属性托管类中由描述符实例处理的公开属性,值存储在储存属性中;
  • 实现了 __get____set__ __delete__ 方法的类是描述符(未实现的操作会忽略描述符,使用默认逻辑)。创建一个描述符实例,作为托管类的类属性,即可使用描述符;
  • 需要注意直接处理托管实例的 __dict__ 属性,避免触发 __set__ 方法无限递归;
  • 注意:描述符的使用不会初始化储存属性。因此在下面的例子中,如果在 __init__ 中不初始化 self.weight,则此时读取 self.weight 会得到 Quantity 对象;在初始化 self.weight 后,再读取 self.weight 则会得到对应的值(此时对于读取操作而言,实例属性覆盖了类属性,这是因为该操作没有被托管给描述符)。
class Quantity:  # <1>  
  
    def __init__(self, storage_name):  
        self.storage_name = storage_name  # <2>  
  
    def __set__(self, instance, value):  # <3> 赋值托管属性
        if value > 0:  
            instance.__dict__[self.storage_name] = value  # <4>  
        else:  
            raise ValueError('value must be > 0')

class LineItem:  
    weight = Quantity('weight')  # <5>  
    price = Quantity('price')  # <6>  
  
    def __init__(self, description, weight, price):  # <7>
        ...

自动生成储存属性的名称

  • 可以在属性名中使用井号 #避免与自定义属性名冲突# 符号是无效的 Python 句法,但 getattrsetattr__dict__ 均可以使用这样的属性名;
  • 可以使用 self.__class__ 获取实例所属的类引用,以得到各类属性,如 __name__ 等;
  • 注意:在创建描述符实例时,还未完成托管类的定义,因此无法得到托管类的名称;
  • __get__ 方法有三个参数:描述符自身 self、托管实例 instance、托管类的引用 ownerowner 可以用于获取类属性等,默认为 instance 实例的类型。如果尝试用获取类属性的方式读取托管属性(LineItem.weight),则 instance=None,此时应当返回描述符本身;
  • Django 模型的字段就是描述符。

20.2 覆盖型与非覆盖型描述符对比

描述符根据是否定义 __set__ 方法分为两大类:覆盖型描述符和非覆盖类描述符。

覆盖型描述符

  • 实现 __set__ 方法覆盖了对实例属性的赋值操作,因此属于覆盖型描述符。可以不实现 __get__ 方法,读操作会直接读实例属性(若不存在则为同名的描述符类属性);
  • 特性也是覆盖型描述符。

非覆盖型描述符

  • 非覆盖型描述符没有实现 __set__ 方法,因此可以直接修改实例属性。同名的实例属性会覆盖描述符,从而导致描述符无法处理该属性;
  • 方法是以非覆盖型描述符实现的

覆盖描述符

  • 依附在类上的描述符无法控制为类属性赋值的操作
  • 可以通过为类属性赋值来覆盖描述符(猴子补丁);
  • 读类属性的操作可用 __get__ 处理,而写类属性的操作不可用 __set__ 处理。

20.3 方法是描述符

  • 在类中定义的函数属于绑定方法。用户定义的函数对象都会含有特殊定义的 __get__ 方法,因此它的作用就是非覆盖型描述符;
  • obj.method 获取的是绑定方法对象Class.method 获取的是定义的函数对象本身。一个是方法(class 'method'),一个是函数(class 'function'
  • 绑定方法对象是可调用的对象,它储存了函数本身,并将托管实例绑定为函数的第一个参数(类似于 functools.partial);
  • 绑定方法对象有以下的属性:
    • __self__ 属性储存调用该方法的实例引用;
    • __func__ 属性储存了托管类原始函数的引用;
    • __call__ 方法实现了可调用,它会调用原始函数,将 __self__ 作为第一个参数传入,用这种方式来隐式绑定形参 self

20.4 描述符用法建议

  • 创建只读属性最简单的方法是使用特性
  • 只读描述符必须有 __set__ 方法,否则会被同名实例属性覆盖;
  • 用于验证的描述符可以只有 __set__ 方法用于设值;
  • 非覆盖型描述符可用于缓存结果:第一次访问该属性时通过描述符执行计算,计算后将结果保存到实例属性中,将描述符覆盖掉;
  • 特殊方法不会被实例属性覆盖:解释器只会在类中寻找对应的特殊方法;
  • 需要大量不受控的动态属性时,应当对动态属性进行过滤、转义,或通过类来直接使用类方法,以此维护数据健全。

20.5 描述符的文档字符串和覆盖删除操作

  • 在描述符类中定义 __delete__ 方法即可处理删除托管属性。