Python 描述符是什么?它和 property 有什么关系?
简化版
描述符是实现了 __get__、__set__ 或 __delete__ 中任意方法的对象,用来接管属性访问逻辑。property 本质上就是一个描述符,它通过描述符协议把方法包装成属性访问。
详细版
描述符示例:
class Positive:
def __set_name__(self, owner, name):
self.name = "_" + name
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.name)
def __set__(self, instance, value):
if value <= 0:
raise ValueError("value must be positive")
setattr(instance, self.name, value)
class Product:
price = Positive()
def __init__(self, price):
self.price = price
调用:
p = Product(10)
p.price = -1 # ValueError
描述符常用于 ORM 字段、表单校验、属性计算、懒加载、方法绑定等场景。
描述符分两类:
- 数据描述符:实现了
__set__或__delete__。 - 非数据描述符:只实现
__get__。
数据描述符优先级高于实例属性;非数据描述符可能被实例属性遮蔽。
完整版教学
一、属性访问不只是查字典
当你写:
obj.x
Python 并不是简单地去 obj.__dict__ 里取 x。如果类属性 x 是描述符,Python 会触发描述符协议。
描述符是一个对象,只要它实现了下面任意方法:
__get__(self, instance, owner)
__set__(self, instance, value)
__delete__(self, instance)
就可以参与属性访问。
二、property 为什么是描述符
看普通 property:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("radius must be positive")
self._radius = value
访问:
c.radius
看起来像读属性,背后会调用 property 对象的 __get__;赋值:
c.radius = 10
背后会调用 property 对象的 __set__。
所以 property 不是语法糖那么简单,它是标准库内置的描述符应用。
三、自己实现一个描述符
比如做正整数校验:
class Positive:
def __set_name__(self, owner, name):
self.private_name = "_" + name
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.private_name)
def __set__(self, instance, value):
if value <= 0:
raise ValueError("must be positive")
setattr(instance, self.private_name, value)
使用:
class Order:
amount = Positive()
def __init__(self, amount):
self.amount = amount
__set_name__ 会在类创建时告诉描述符:你被绑定到了哪个类的哪个属性名上。
四、数据描述符和非数据描述符
数据描述符实现了 __set__ 或 __delete__:
class DataDescriptor:
def __get__(self, instance, owner):
...
def __set__(self, instance, value):
...
非数据描述符只实现 __get__:
class NonDataDescriptor:
def __get__(self, instance, owner):
...
差异在属性查找优先级上:
- 数据描述符优先于实例字典;
- 实例字典优先于非数据描述符。
这也是为什么 property 带 setter 时很难被实例属性直接绕过。
五、方法绑定也和描述符有关
类里的函数对象也是描述符。
class User:
def hello(self):
return "hello"
u = User()
u.hello()
当访问 u.hello 时,函数的 __get__ 会把函数和实例绑定起来,形成绑定方法。于是调用时 Python 会自动传入 self。
所以描述符是 Python 对象模型的底层机制之一,不只是 property 的实现细节。
六、描述符适合哪些场景
描述符适合把一类属性访问规则复用起来。
例如:
- ORM 字段映射;
- 字段类型校验;
- 懒加载属性;
- 只读属性;
- 缓存属性;
- 框架级声明式 API。
如果只是一个类的一个简单属性,用 property 更直接;如果同样规则要复用到很多字段,描述符更合适。
七、常见误区与追问
记忆钩子:描述符就是“属性访问拦截器”。只要类属性对象实现了
__get__、__set__、__delete__中的相关方法,访问实例属性时就可能先进入它。
- 误区:描述符只和
property有关。property是描述符的典型应用,但普通函数变成绑定方法、staticmethod、classmethod、ORM 字段也都依赖描述符机制。 - 追问:数据描述符和非数据描述符谁优先级更高? 数据描述符实现
__set__或__delete__,优先级高于实例字典;非数据描述符通常会被实例同名属性遮蔽。 - 误区:描述符实例会为每个对象单独创建。 描述符通常作为类属性存在,一个描述符对象被这个类的多个实例共享,所以真实数据常放到实例
__dict__或其他按实例隔离的存储里。 - 追问:
__get__(self, instance, owner)里instance什么时候是None? 通过类访问属性时,例如User.name,instance是None;描述符可以在这种情况下返回自身,方便调试或框架收集字段定义。 - 误区:有了描述符就应该替代所有 getter/setter。 描述符适合复用属性管理逻辑,例如校验、转换、延迟加载;单个简单属性用
property往往更轻。 - 追问:为什么方法调用会自动传入 self? 函数对象实现了描述符协议,通过实例访问函数时会返回绑定方法,把实例绑定为第一个参数;所以
obj.method()才等价于Class.method(obj)。
八、加强记忆
描述符就是“能接管属性读写删除的对象”,核心方法是 __get__、__set__、__delete__。property 是描述符的内置应用,方法绑定也依赖描述符。面试时讲到这里,Python OOP 的底层味道就出来了。