Python 属性描述符(Descriptor) 是 Python 面向对象中最强大、最被低估的核心机制之一。它被誉为“Python 元编程的基石”,直接支撑了:
@property、@classmethod、@staticmethod- 函数绑定为方法(bound method)
- Django ORM / SQLAlchemy / Peewee 等所有主流 ORM 的字段(Field/Column)
- 类型验证、懒加载、缓存、代理、数据校验等高级特性
掌握描述符,你就真正打开了“理解 Python 黑魔法”的大门。
下面从零基础原理 → 底层机制 → 自定义实战 → ORM 深度应用(基于 Python 3.12~3.14 最新特性,2026 年 3 月视角)进行系统详解。
1. 什么是描述符?一句话定义
描述符就是一个实现了描述符协议的对象,当它作为类属性存在时,会接管该属性在实例上的 get/set/delete 操作。
描述符协议(Descriptor Protocol)只有 4 个特殊方法:
class Descriptor:
def __get__(self, instance, owner=None):
"""instance 是实例,owner 是类"""
...
def __set__(self, instance, value):
...
def __delete__(self, instance):
...
def __set_name__(self, owner, name): # Python 3.6+
"""自动记录属性名,超级好用"""
self.name = name
只要实现了 __get__ / __set__ / __delete__ 中的至少一个,就是一个描述符。
2. 数据描述符 vs 非数据描述符(最核心区别!)
| 类型 | 必须实现的方法 | 优先级(相对于实例 dict) | 典型例子 |
|---|---|---|---|
| 数据描述符 | __get__ + __set__(或 __delete__) | 高于实例字典 | @property、ORM Field |
| 非数据描述符 | 只实现 __get__ | 低于实例字典 | 函数、classmethod、staticmethod |
关键规则(背下来):
- 数据描述符优先级最高:即使实例
__dict__中有同名键,也走描述符的__get__/__set__ - 非数据描述符优先级最低:实例有同名属性时,直接返回实例的,不会调用描述符
这也是为什么 @property 可以“覆盖”实例属性,而普通方法不行。
3. 属性查找完整流程(getattribute 机制)
当你执行 obj.attr 时,Python 实际调用的是 type(obj).__getattribute__(obj, 'attr'),其查找顺序(极简版):
- 在类和基类中查找
attr - 如果找到的是数据描述符 → 立即调用
desc.__get__(obj, type(obj)) - 如果找到的是非数据描述符 或 普通属性 → 先检查
obj.__dict__ - 实例
__dict__有 → 返回 - 否则调用非数据描述符的
__get__ - 再找
__getattr__(兜底)
这套机制让描述符成为“属性劫持神器”。
4. 内置描述符拆解
4.1 @property(最常用数据描述符)
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not value:
raise ValueError("名字不能为空")
self._name = value
底层其实是:
name = property(fget=..., fset=..., fdel=..., doc=...)
# property 类实现了 __get__、__set__、__delete__
4.2 classmethod / staticmethod(非数据描述符)
它们只实现了 __get__,所以实例可以“覆盖”它们(极少见)。
4.3 函数本身也是描述符!
def func(self): pass
print(func.__get__(None, Person)) # <function func at ...> → unbound
print(func.__get__(Person(), Person)) # <bound method ...>
这正是“实例调用方法自动传 self”的秘密。
5. 自定义描述符实战(从简单到高级)
示例1:类型+范围验证器(最常见)
class Typed:
def __set_name__(self, owner, name):
self.name = name # Python 3.6+ 神器
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError(f"{self.name} 必须是 {self.expected_type}")
if self.validator and not self.validator(value):
raise ValueError(f"{self.name} 校验失败")
instance.__dict__[self.name] = value # 存到实例字典
class PositiveInt(Typed):
expected_type = int
validator = lambda x: x > 0
class User:
age = PositiveInt()
score = PositiveInt()
u = User()
u.age = 18 # OK
u.age = -5 # TypeError / ValueError
示例2:懒加载描述符(数据库/大文件场景)
class LazyAttribute:
def __init__(self, func):
self.func = func
self.__name__ = func.__name__
def __get__(self, instance, owner):
if instance is None:
return self
value = self.func(instance)
instance.__dict__[self.__name__] = value # 缓存到实例
return value
class Article:
@LazyAttribute
def content(self):
print("从数据库加载大文本...")
return "很长的文章内容..." * 1000
示例3:缓存描述符(带过期时间)
可扩展为 Redis 后端缓存。
6. ORM 实践:描述符如何驱动整个模型层
Django ORM(经典实现)
class ModelBase(type):
def __new__(mcs, name, bases, attrs):
for key, value in attrs.items():
if isinstance(value, Field): # Field 是数据描述符
value.set_name(name, key) # 记录字段名
return super().__new__(mcs, name, bases, attrs)
class Field:
def __set_name__(self, owner, name):
self.attname = name # 数据库列名
def __get__(self, instance, owner):
if instance is None:
return self # 类访问返回 Field 本身(用于 QuerySet)
return instance.__dict__[self.attname]
def __set__(self, instance, value):
# 类型转换、校验、信号触发等
instance.__dict__[self.attname] = value
class CharField(Field):
def __init__(self, max_length):
self.max_length = max_length
当你写:
class User(models.Model):
name = models.CharField(max_length=100)
name 其实是一个 CharField 实例(数据描述符)!
访问 user.name → 走 __get__user.name = "重阳" → 走 __set__(校验长度、触发 pre_save 等)
SQLAlchemy 2.0+(更现代)
class Column:
def __set_name__(self, owner, name):
...
# InstrumentedAttribute 是真正的描述符
# 通过 __get__/__set__ 实现与 Session、查询、事件系统的深度集成
SQLAlchemy 使用了更复杂的 InstrumentedAttribute + QueryableAttribute,但核心仍是描述符协议。
7. 高级技巧与注意事项(2026 生产必知)
- set_name 是救命稻草
Python 3.6+ 引入,自动传递属性名,彻底解决“硬编码属性名”的痛点。 - 描述符 + metaclass 是框架标配
ORM 都在 metaclass 中收集所有 Field 描述符。 - 槽位类(slots)与描述符
__slots__会阻止实例__dict__创建,数据描述符仍可正常工作。 - 常见陷阱
- 把描述符实例放在实例属性而不是类属性 → 失效
- 非数据描述符被实例属性覆盖后永久失效
- 多继承时描述符查找顺序(MRO)
- 线程安全(描述符本身无锁,需自行处理)
- 性能
数据描述符每次访问都会走 Python 方法调用,比普通属性慢 10~30%,但 ORM 场景完全可接受。
8. 一张图总结(文字版)
实例.attr
↓
type(实例).__getattribute__
↓
找到类属性是描述符?
├── 数据描述符 → 直接调用 __get__/__set__(绕过实例dict)
└── 非数据描述符 → 先查实例.__dict__,没有再调用 __get__
最后总结口诀
- 想“劫持”属性访问 → 用描述符
@property只是语法糖,底层就是描述符- ORM 的 Field/Column 本质都是描述符
- 自定义描述符 = 复用 + 声明式编程 + 框架级能力
掌握描述符后,你再看 Django/SQLAlchemy 的源码会瞬间通透,也能自己写出优雅的配置类、验证框架、缓存系统。
推荐深入阅读(2026 最新):
- 官方文档:https://docs.python.org/3/howto/descriptor.html
- 《Fluent Python》 第 20 章(描述符章节极佳)
- SQLAlchemy / Django 源码中的
fields.py和attributes.py
你现在最想看哪一部分的完整可运行代码?
- 手写一个迷你 ORM(支持查询、校验、迁移)
- SQLAlchemy 描述符源码拆解
- 描述符实现 FastAPI Pydantic-like 自动验证
- 线程安全的缓存描述符
告诉我,我立刻给你对应完整项目级代码!重阳,继续冲!描述符一旦掌握,Python 进阶之路就真正打开了。🚀