Python中的属性描述符与描述符协议
字数 1450 2025-12-05 09:57:24
Python中的属性描述符与描述符协议
描述符是Python中一种高级特性,它允许对象自定义属性访问行为。描述符协议由三个特殊方法组成:__get__、__set__ 和 __delete__。任何实现了至少其中一个方法的类,都可以称为描述符。描述符常用于实现属性验证、延迟计算、数据绑定等功能。
1. 描述符的基本概念
- 描述符:是一个类,其实例被另一个类的类属性所引用,并通过描述符协议控制对该属性的访问。
- 描述符类型:
- 数据描述符:实现了
__set__或__delete__方法(通常同时实现__get__)。 - 非数据描述符:只实现了
__get__方法。
- 数据描述符:实现了
- 关键点:数据描述符的优先级高于实例字典,而非数据描述符的优先级低于实例字典。
2. 描述符协议方法详解
a) __get__(self, instance, owner)
- 当从实例或类访问描述符属性时调用。
- 参数:
self:描述符实例本身。instance:访问属性的实例。如果通过类访问,则为None。owner:拥有该描述符的类。
- 返回值:可以是任意值,通常与属性访问的预期类型一致。
b) __set__(self, instance, value)
- 当对描述符属性赋值时调用。
- 参数:
self:描述符实例。instance:操作属性的实例。value:要设置的值。
- 无返回值。
c) __delete__(self, instance)
- 当删除描述符属性(使用
del)时调用。 - 参数:
self:描述符实例。instance:操作属性的实例。
- 无返回值。
3. 创建描述符的步骤
步骤1:定义描述符类
假设我们创建一个验证正整数的描述符:
class PositiveInteger:
def __get__(self, instance, owner):
# 返回存储的值
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
if not isinstance(value, int) or value <= 0:
raise ValueError("必须为正整数")
# 将值存储在实例字典中
instance.__dict__[self.name] = value
def __delete__(self, instance):
del instance.__dict__[self.name]
步骤2:在目标类中使用描述符
class Product:
# 类属性为描述符实例
quantity = PositiveInteger()
price = PositiveInteger()
def __init__(self, name, quantity, price):
self.name = name
self.quantity = quantity
self.price = price
但此时 PositiveInteger 中的 self.name 未定义。我们需要让描述符知道它被赋值的属性名。
步骤3:添加属性名绑定
通常通过元类或 __set_name__ 方法(Python 3.6+)实现:
class PositiveInteger:
def __set_name__(self, owner, name):
# 设置描述符对应的属性名
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
if not isinstance(value, int) or value <= 0:
raise ValueError("必须为正整数")
instance.__dict__[self.name] = value
def __delete__(self, instance):
del instance.__dict__[self.name]
现在 Product 类可以正常工作:
p = Product("apple", 10, 5) # 有效
p.quantity = 20 # 有效
p.quantity = -5 # 抛出 ValueError
4. 数据描述符 vs 非数据描述符
- 数据描述符:实现了
__set__或__delete__。
示例:上面的PositiveInteger是数据描述符。 - 非数据描述符:只实现
__get__。
示例:@property装饰器创建的属性是非数据描述符(除非定义了 setter)。
优先级测试:
class DataDescriptor:
def __get__(self, instance, owner):
return "数据描述符"
def __set__(self, instance, value):
pass
class NonDataDescriptor:
def __get__(self, instance, owner):
return "非数据描述符"
class Test:
data = DataDescriptor()
non_data = NonDataDescriptor()
obj = Test()
obj.__dict__['data'] = '实例属性'
obj.__dict__['non_data'] = '实例属性'
print(obj.data) # 输出:数据描述符(数据描述符优先)
print(obj.non_data) # 输出:实例属性(非数据描述符优先级低)
5. 描述符的应用场景
- 属性验证:如上面的
PositiveInteger。 - 延迟计算:将耗时计算的结果缓存。
- 数据绑定:在GUI框架中自动更新界面。
- 实现
@property:property 本身是基于描述符实现的装饰器。
6. 描述符与 property 的关系
@property 是描述符的语法糖。以下两种方式等价:
# 使用 property
class MyClass:
@property
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = value
# 使用描述符(近似实现)
class MyClass:
x = property(lambda self: self._x, lambda self, v: setattr(self, '_x', v))
property 是一个内置的数据描述符类。
7. 注意事项
- 描述符实例是类属性,所有实例共享同一个描述符对象。
- 通过
__dict__存储实例数据,避免描述符自身的数据在实例间共享。 - 使用
__set_name__简化属性名绑定(Python 3.6+)。 - 数据描述符优先级高,可能覆盖实例字典中的同名属性。
通过以上步骤,你可以理解描述符如何工作,并能够自定义描述符来实现高级属性控制。