结论:描述符是定义了 __get__ / __set__ / __delete__ 中任意方法的对象,作为类属性存在时,对它的访问会被解释器转发到这些方法。它是 Python 属性访问的底层机制:property、实例方法的绑定(函数就是描述符)、classmethod、staticmethod 全部由描述符实现。
展开:调用规约: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__ 与描述符在属性查找链中的先后。