描述符对象是 Python 对象模型中一个核心且强大的机制,它支撑着语言的许多动态特性。从最基础的属性访问,到复杂框架(如 Django ORM、SQLAlchemy、Pydantic 的字段系统)的内部实现,描述符都在幕后扮演着关键角色,精细地控制着整个属性系统的行为。
如果说 __dict__ 提供了属性数据的静态存储结构,那么描述符就是在这之上进行动态访问控制的干预层。
需要明确的是,描述符并非特殊语法或魔法,它是完全遵循 Python 对象模型的普通对象。
一、描述符的基本概念
1. 描述符是对象
在 Python “一切皆对象”的理念下,描述符也不例外:
- 它是某个类的实例。
- 拥有自身的类型、属性与方法。
- 可以被赋值、传递并存储在
__dict__ 中。
class Descriptor:
pass
d = Descriptor()
在这一层面上,d 与任何普通对象没有区别。
2. 描述符语义的由来
描述符之所以能获得特殊语义,并非源于其“身份”,而在于它实现了特定的协议方法,并且被放置在了类属性的位置上。
当一个对象同时满足以下两个条件时,在属性访问过程中就会被 Python 解释器识别为描述符:
- 实现了
__get__()、__set__()、__delete__() 中至少一个方法。
- 作为类属性存在于另一个类的
__dict__ 中。
二、描述符的存储位置与作用范围
1. 描述符的存储位置
描述符对象要参与属性访问控制,必须作为类属性存在于目标类的 __dict__ 中。
class D:
def __get__(self, obj, owner):
return “descriptor”
class A:
x = D() # 描述符对象存放在 A.__dict__ 中
这里的 D() 实例是一个普通对象,但由于它位于 A.__dict__ 中,因此会介入属性查找链。
2. 描述符的作用对象
尽管描述符本身存储在类级别,但它控制的是:
- 实例属性的访问行为。
- 类属性的访问行为(当通过类直接访问时,
instance 参数为 None)。
例如:
a = A()
print(a.x) # 输出:descriptor
这次访问在底层被解释为:
A.__dict__['x'].__get__(a, A)
从语言规范角度看,描述符对象本质上是对描述符协议的实现。这些协议方法并非“魔法”,而是 Python 在属性查找过程中主动调用的标准接口。如果你想深入学习 Python 对象模型,可以参考Python编程相关专题。
三、描述符的分类
根据是否拦截属性的写入或删除操作,描述符可分为两类:数据描述符和非数据描述符。
1. 数据描述符
定义:实现了 __set__() 和/或 __delete__() 方法的描述符,通常也会实现 __get__()。
行为特征:在属性查找顺序中,其优先级高于实例的 __dict__。因此实例无法通过定义同名属性来绕过它的控制。
示例:确保值为非负数
class Positive:
"""数据描述符:确保值为非负数"""
def __set_name__(self, owner, name):
# 将实际存储的字段名设为 _<name>
self.storage_name = f"_{name}"
def __get__(self, obj, owner):
if obj is None:
return self
# 从私有备份字段中取值
return obj.__dict__.get(self.storage_name)
def __set__(self, obj, value):
if value < 0:
raise ValueError("值必须为非负数!")
# 将合法值存入私有备份字段
obj.__dict__[self.storage_name] = value
使用与验证:
class Account:
balance = Positive()
a = Account()
a.balance = 100 # 调用 Positive.__set__
print(a.balance) # 调用 Positive.__get__,输出 100
# a.balance = -100 # 抛出 ValueError: 值必须为非负数!
# 尝试绕过描述符
a.__dict__['balance'] = -999 # 这是在实例字典中添加了一个干扰项
print(a.balance) # 依然输出 100!因为描述符优先级更高
print(a.__dict__) # 输出 {'_balance': 100, 'balance': -999}
说明:数据描述符确保了通过“正规途径”(即 obj.attr = value)赋值的数据一定是合法的。真正的保护机制通常将存储名(如 _balance)与对外暴露的属性名(balance)分离。
2. 非数据描述符
定义:仅实现了 __get__() 方法的描述符。
行为特征:优先级低于实例的 __dict__,因此可以被实例的同名属性“遮蔽”。
示例:实现惰性求值
class LazyValue:
"""非数据描述符:首次访问时计算并缓存结果"""
def __init__(self, func):
self.func = func
def __get__(self, obj, owner):
if obj is None:
return self
value = self.func(obj)
# 将计算结果缓存到实例字典中
obj.__dict__[self.func.__name__] = value
return value
使用与验证:
class Data:
@LazyValue
def value(self):
print(“computing...”)
return 42
d = Data()
print(“第一次访问:”)
print(d.value) # 输出 “computing...” 然后输出 42
print(“第二次访问:”)
print(d.value) # 直接输出 42,不再打印 “computing...”
说明:此例利用了非数据描述符优先级低的特性来实现“惰性求值”。首次访问触发计算并将结果存入 d.__dict__;后续访问时,实例属性直接“遮蔽”了描述符,从而快速返回缓存值,优化了性能。
四、Python 内置的描述符对象
Python 的许多核心功能本身就是由描述符实现的。
1. 函数对象(非数据描述符)
类中定义的普通方法,其函数对象本身就是非数据描述符。通过它的 __get__() 方法,Python 实现了实例方法的自动绑定。
class A:
def foo(self):
pass
a = A()
访问 a.foo 的本质是:
A.__dict__['foo'].__get__(a, A)
这会返回一个绑定了实例 a 的绑定方法。
2. @property(数据描述符)
@property 装饰器返回一个标准的数据描述符对象(实现了 __get__, __set__, __delete__),用于将属性访问优雅地映射到方法调用上。
class Person:
def __init__(self, age):
self._age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if value < 0:
raise ValueError(“age must be >= 0”)
self._age = value
可以说,@property 是描述符机制的一个官方、易用的封装版本。
3. @classmethod 与 @staticmethod
这两个装饰器也返回描述符对象,分别实现了对类对象或函数本身的不同绑定策略。
class Demo:
x = 10
@classmethod
def cls_method(cls):
return cls.x
@staticmethod
def static_method():
return “no binding”
classmethod 描述符在 __get__() 中将 owner(类)绑定为第一个参数;staticmethod 描述符则在 __get__() 中直接返回原始函数,不做绑定。两者都是描述符,只是绑定策略不同。
五、描述符在属性查找链中的位置
当执行 obj.attr 时,Python 的解释器遵循以下查找顺序:
- 类
__dict__ 中的数据描述符。
- 实例
obj.__dict__。
- 类
__dict__ 中的非数据描述符。
- 类
__dict__ 中的普通属性。
- 父类链(遵循 MRO)。
__getattr__() 方法。
描述符的“权力”是由这个协议和查找顺序共同赋予的,而非绝对的魔法。
六、现代最佳实践:__set_name__
Python 3.6 引入了 __set_name__(self, owner, name) 方法。它在类创建阶段被自动调用,让描述符能获知自己在所属类中的属性名,这已成为现代描述符实现的标准范式。
示例:类型检查描述符
class Typed:
def __set_name__(self, owner, name):
# 在所属类创建时自动调用
self.storage_name = f"_{name}"
def __get__(self, obj, owner):
if obj is None:
return self
return getattr(obj, self.storage_name)
def __set__(self, obj, value):
if not isinstance(value, int):
raise TypeError(“Value must be int”)
setattr(obj, self.storage_name, value)
使用:
class Employee:
age = Typed() # 解释器会隐式调用 Typed.__set_name__(Employee, “age”)
salary = Typed() # 解释器会隐式调用 Typed.__set_name__(Employee, “salary”)
e = Employee()
e.age = 30 # 调用 Typed.__set__(e, 30)
e.salary = 8000 # 调用 Typed.__set__(e, 8000)
print(e.age) # 输出:30
print(e.salary) # 输出:8000
# e.__dict__ 内容为 {“_age”: 30, “_salary”: 8000}
说明:这是现代描述符的典型模式,也是许多 ORM(对象关系映射)框架和字段系统的设计基石。数据实际存储在实例的 __dict__ 中,但所有访问都必须经过类 __dict__ 中的描述符对象进行验证和控制。这类技术常用于构建数据模型,与数据库/中间件技术结合紧密。
小结
描述符对象是 Python 属性系统的支柱。它们以普通对象之身,通过实现特定的协议方法,深度介入属性查找过程,实现了对属性访问、赋值、删除等行为的精细控制。掌握描述符,是理解 Python 对象模型、进行高级元编程和框架开发的关键一步,也是深入使用诸如Django ORM等高级库的基础。