Python 编程 - 描述符协议
描述符协议(Descriptor Protocol)是 Python 中一个核心且强大的底层机制。它允许你自定义对象属性的访问、赋值和删除行为。
简单来说,描述符就是实现了特定协议方法的 Python 对象,它作为一种“可重用的属性逻辑”存在,可以让你优雅地管理属性。
我们日常使用的许多 Python 高级特性,如 @property、@classmethod、@staticmethod,甚至是函数绑定机制,其底层都是由描述符协议实现的。
📜 描述符协议的核心方法
一个对象要成为描述符,需要实现以下一个或多个方法:
| 方法 | 签名 | 触发时机 |
|---|---|---|
__get__ |
__get__(self, instance, owner) |
读取属性时 (如 obj.attr) |
__set__ |
__set__(self, instance, value) |
赋值属性时 (如 obj.attr = value) |
__delete__ |
__delete__(self, instance) |
删除属性时 (如 del obj.attr) |
__set_name__ |
__set_name__(self, owner, name) |
(Python 3.6+) 类创建时自动调用,告知描述符其被赋值的属性名 |
这些方法的参数含义如下:
self: 描述符实例本身。instance: 拥有该属性的实例对象(通过类访问时为None)。owner: 拥有该属性的类。value: 要赋的新值。
重要前提:描述符协议只有在描述符对象被作为类属性(即定义在类体中)时才会生效。如果将其赋值给某个实例,则它只是一个普通对象,协议方法不会被触发。
⚖️ 数据描述符 vs. 非数据描述符
根据实现的方法,描述符分为两类,它们在属性查找时的优先级不同:
- 数据描述符 (Data Descriptor):同时实现了
__get__和__set__方法。它的优先级最高,会覆盖实例属性(instance.__dict__)。 - 非数据描述符 (Non-Data Descriptor):只实现了
__get__方法。它的优先级较低,只有在实例属性(instance.__dict__)中找不到对应属性时才会被触发。
这个优先级区别是理解 Python 属性查找机制的关键。
🔍 属性查找的完整链条
当你访问一个对象的属性(如 obj.attr)时,Python 遵循以下查找顺序:
- 查找数据描述符:在
obj的类及其父类中查找attr。如果它是一个数据描述符,则直接调用其__get__方法并返回,查找结束。 - 查找实例属性:检查
obj.__dict__字典。如果找到attr,则直接返回其值,查找结束。 - 查找非数据描述符:在
obj的类及其父类中查找attr。如果它是一个非数据描述符,则调用其__get__方法并返回。 - 查找普通类属性:如果仍然没找到,则返回类中定义的普通属性。
- 触发
__getattr__:如果以上都未找到,且类中定义了__getattr__方法,则调用它。 - 抛出
AttributeError:最终仍未找到,则抛出AttributeError。
💡 典型应用场景
描述符在 Python 生态中被广泛应用:
- 数据验证与类型检查:在赋值时自动验证数据的合法性。
- 惰性计算 (Lazy Evaluation):属性只在首次访问时进行计算,节省资源。
- ORM(对象关系映射):如 Django 的
models.Field和 SQLAlchemy 的列类型,都是描述符。 - 实现
@property:将方法调用变成属性访问。 - 实现
@classmethod和@staticmethod:改变方法的调用方式。 - 缓存与日志记录:在属性访问时添加额外的缓存或日志逻辑。
💻 实战示例:实现带类型检查的属性
下面这个例子展示了如何使用描述符创建一个支持类型检查的属性:
class TypedProperty:
def __init__(self, expected_type):
self.expected_type = expected_type
def __set_name__(self, owner, name):
# 获取属性在类中的名字,例如 'name' 或 'age'
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
# 从实例的 __dict__ 中获取值
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
# 在赋值时进行类型检查
if not isinstance(value, self.expected_type):
raise TypeError(f"Expected {self.expected_type.__name__}, got {type(value).__name__}")
# 验证通过,将值存入实例的 __dict__
instance.__dict__[self.name] = value
class Person:
# 将描述符作为类属性使用
name = TypedProperty(str)
age = TypedProperty(int)
def __init__(self, name, age):
self.name = name
self.age = age
现在,Person 类的属性拥有了类型检查的能力:
>>> p = Person("Alice", 30)
>>> p.name
'Alice'
>>> p.age
30
>>> p.name = 123 # 尝试赋一个非字符串值
Traceback (most recent call last):
...
TypeError: Expected str, got int
在这个例子中,TypedProperty 类实现了描述符协议。__set_name__ 让它知道自己在 Person 类中的属性名,__get__ 和 __set__ 则接管了对该属性的读写操作,并在写入时加入了类型校验逻辑。
描述符协议是理解 Python 底层运作和进行高级编程的利器,它让你能够以优雅、可复用的方式控制属性行为。

浙公网安备 33010602011771号