什么是描述符协议?
描述符(Descriptor)是 Python 中一个强大但常被忽视的特性。它是实现属性访问协议的核心机制,让我们能够自定义属性的获取、设置和删除行为。
简单来说,描述符是实现了 __get__ 、 __set__ 或 __delete__ 方法的对象。当一个描述符对象作为类属性存在时,对该属性的访问会被描述符方法拦截。
描述符协议的三个核心方法
python
class Descriptor:
def __get__(self, obj, objtype=None):
"""获取属性值时调用"""
if obj is None:
return self
# 返回属性值
return ...
def __set__(self, obj, value):
"""设置属性值时调用"""
# 验证并存储值
...
def __delete__(self, obj):
"""删除属性时调用"""
# 清理操作
...
__get__: 访问属性时触发,obj是实例,objtype是类__set__: 赋值时触发,实现了此方法的是数据描述符__delete__: 删除属性时触发
实战:实现类型检查描述符
让我们创建一个实用的类型检查描述符,确保属性只能被赋予指定类型的值:
python
class Typed:
"""类型检查描述符"""
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
def __get__(self, obj, objtype=None):
if obj is None:
return self
# 从实例的 __dict__ 获取值
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(
f'{self.name} 必须是 {self.expected_type.__name__}, '
f' got {type(value).__name__}'
)
obj.__dict__[self.name] = value
def __delete__(self, obj):
if self.name in obj.__dict__:
del obj.__dict__[self.name]
# 使用描述符的类
class Person:
name = Typed('name', str)
age = Typed('age', int)
salary = Typed('salary', float)
def __init__(self, name, age, salary):
self.name = name # 触发 Typed.__set__
self.age = age
self.salary = salary
def __repr__(self):
return f'Person({self.name}, {self.age}, {self.salary})'
# 测试
p = Person("张三", 25, 8500.0)
print(p) # Person(张三,25, 8500.0)
# 类型检查生效
try:
p.age = "二十五" # TypeError!
except TypeError as e:
print(f"捕获错误:{e}")
描述符的优先级
理解描述符的优先级对于调试至关重要:
- 数据描述符 (实现了
__set__)> 实例字典 - 实例字典 > 非数据描述符 (只实现
__get__) - 类属性 >
__getattr__
ruby
class DataDesc:
def __get__(self, obj, objtype=None):
return "data_desc"
def __set__(self, obj, value):
obj.__dict__['temp'] = value
class NonDataDesc:
def __get__(self, obj, objtype=None):
return "non_data_desc"
class Test:
data = DataDesc()
non_data = NonDataDesc()
t = Test()
t.data = "instance_value" # 数据描述符优先,但值存入 __dict__
print(t.data) # 仍输出 "data_desc"(数据描述符拦截)
print(t.non_data) # "non_data_desc"
t.__dict__['non_data'] = "direct"
print(t.non_data) # "direct"(实例字典优先于非数据描述符)
实用场景:懒加载属性
描述符非常适合实现懒加载(Lazy Loading),即属性值在首次访问时才计算:
python
class LazyProperty:
"""懒加载属性描述符"""
def __init__(self, func):
self.func = func
self.name = func.__name__
def __get__(self, obj, objtype=None):
if obj is None:
return self
# 首次访问时计算并缓存
value = self.func(obj)
obj.__dict__[self.name] = value
return value
class Image:
def __init__(self, filepath):
self.filepath = filepath
@LazyProperty
def data(self):
"""仅在首次访问时加载图像数据"""
print(f"正在加载 {self.filepath}...")
# 模拟耗时操作
import time
time.sleep(1)
return b"image_data_placeholder"
img = Image("photo.jpg")
print("Image 对象已创建,但数据尚未加载")
print(img.data) # 此时才加载
print(img.data) # 直接从缓存获取,不再加载
与 @property 的区别
@property 实际上是描述符的语法糖。对比一下:
ruby
# 使用 @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("半径不能为负")
self._radius = value
# 使用描述符实现相同功能
class Positive:
def __init__(self, name):
self.name = name
def __get__(self, obj, objtype=None):
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if value < 0:
raise ValueError(f"{self.name} 不能为负")
obj.__dict__[self.name] = value
class CircleDesc:
radius = Positive('radius')
def __init__(self, radius):
self.radius = radius
选择建议:
- 简单场景用
@property,语法更简洁 - 需要复用时用描述符,避免重复代码
- 框架开发中描述符更灵活
小结
描述符协议是 Python 面向对象编程的基石之一:
| 特性 | 说明 |
|---|---|
| 数据描述符 | 实现 __set__,优先级高于实例字典 |
| 非数据描述符 | 只实现 __get__,优先级低于实例字典 |
| 典型应用 | 类型检查、懒加载、属性验证、ORM 字段 |
| 与 property 关系 | @property 是描述符的语法糖 |
掌握描述符,你就能理解 Python 属性访问的底层机制,写出更优雅、更可复用的代码。