结论:描述符是定义了 __get__ / __set__ / __delete__ 中任意方法的对象,作为类属性存在时,对它的访问会被解释器转发到这些方法。它是 Python 属性访问的底层机制:property、实例方法的绑定(函数就是描述符)、classmethodstaticmethod 全部由描述符实现。

展开:调用规约:obj.attr 时若类上存在数据描述符(同时定义 __get____set__/__delete__),优先调用它的 __get__(obj, type);否则读实例 __dict__;再否则非数据描述符(只有 __get__);最后才是类属性。这个优先级解释了:1)实例方法为何绑定——函数实现了 __get__,通过类访问得原函数、通过实例访问得 bound method(self 被塞入);2)property 为何不能被实例字典遮蔽——它是数据描述符;3)缓存属性技巧 functools.cached_property 为何可行——非数据描述符首次计算后写入实例 __dict__,之后实例字典优先命中。易错点:描述符必须定义在上,塞进实例 __dict__ 不生效;__set_name__ 钩子(3.6+)让描述符知道自己被绑定到哪个属性名,ORM 字段声明靠它省去重复写名字。

class Positive:
    def __set_name__(self, owner, name):
        self.name = name
    def __get__(self, obj, objtype=None):
        return obj.__dict__[self.name]
    def __set__(self, obj, value):
        if value <= 0: raise ValueError("必须为正")
        obj.__dict__[self.name] = value

class Order:
    amount = Positive()

追问方向:Django/SQLAlchemy 的字段如何用描述符实现、__getattr__ 与描述符在属性查找链中的先后。