Python 属性描述符(Descriptor):从原理到 ORM 实践详解(2026 年视角)
属性描述符是 Python 中最底层、最强大却最被低估的特性之一。它是 @property、@classmethod、@staticmethod、方法绑定、SQLAlchemy Column、Django Field 等“魔法”的真正底层实现。
掌握描述符后,你会突然明白:
- 为什么
user.name = "张三"能自动触发数据库更新? - 为什么
User.name(类上)能生成 SQL 表达式? - 为什么
@property能同时实现 getter 和 setter?
下面从原理 → 实现 → ORM 实战,给你一次系统性深度解析(基于 Python 3.12+ / SQLAlchemy 2.0+)。
1. 什么是描述符?一句话定义
描述符就是一个实现了 __get__、__set__ 或 __delete__ 方法的类实例。
当它作为类属性存在时,Python 会把对该属性的访问/赋值/删除操作“劫持”给它。
描述符必须定义在类上(放在实例上无效)——这是核心规则。
2. 描述符协议(Descriptor Protocol)
一个类只要实现以下任意一个方法,就成为描述符:
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__:del 时调用__set_name__:类定义完成后自动调用,告诉你“我叫什么名字”
3. 数据描述符 vs 非数据描述符(决定优先级)
| 类型 | 是否实现 __set__/__delete__ | 优先级 | 典型例子 | 查找顺序影响 |
|---|---|---|---|---|
| 数据描述符 | 是 | 最高 | @property(带 setter)、SQLAlchemy Column | 优先于实例 __dict__ |
| 非数据描述符 | 否(只实现 __get__) | 最低 | 函数、方法、@classmethod、@staticmethod | 实例 __dict__ 优先于它 |
属性查找完整顺序(Python 底层 __getattribute__ 逻辑):
- 类上存在数据描述符 → 直接调用
__get__ - 实例
__dict__中有该属性 → 返回实例值 - 类上存在非数据描述符 → 调用
__get__ - 否则调用
__getattr__(如果有)
这就是为什么:
@property(数据描述符)能覆盖实例属性- 普通方法(非数据描述符)会被实例同名属性覆盖
4. 手写描述符实战示例
示例1:带验证的描述符(最经典)
class Validated:
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, str) or len(value) < 3:
raise ValueError(f"{self.name} 必须是至少3个字符的字符串")
instance.__dict__[self.name] = value
class User:
name = Validated() # 使用描述符
u = User()
u.name = "张三" # 验证通过
# u.name = "张" # 报错
print(u.name)
示例2:懒加载描述符(缓存)
class Lazy:
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:
@Lazy
def content(self):
print("正在从数据库加载...")
return "很长的文章内容..."
5. 内置描述符应用(你每天都在用)
@property→ 数据描述符(property类同时实现了__get__和__set__)- 普通方法 → 非数据描述符(函数对象有
__get__,返回 bound method) @classmethod、@staticmethod→ 也是描述符functools.cached_property(Python 3.8+)→ 数据描述符 + 缓存
6. ORM 实践:SQLAlchemy 如何利用描述符(核心)
SQLAlchemy 是描述符在 ORM 中最完美的应用案例。
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column()
age: Mapped[int] = mapped_column()
背后发生了什么?
- 类定义完成后,SQLAlchemy 的 instrumentation 系统会把
Column对象替换成InstrumentedAttribute(一个数据描述符)。 User.id(类上访问)→InstrumentedAttribute.__get__返回 SQL 表达式对象(User.id == 5能生成 SQL)。user = session.get(User, 1)后user.name(实例访问)→__get__触发懒加载、类型转换、事件监听等。user.name = "李四"→__set__触发脏数据标记(dirty)、事件、验证等。
这就是为什么你写 user.name = xxx 就能自动同步到数据库,却感觉不到任何“魔法”——全靠描述符!
Django ORM 也一样:每个 models.Field(CharField、IntegerField 等)都是描述符,在 __get__/__set__ 中处理数据库映射、验证、to_python 等。
7. 2026 年最佳实践与高级用法
- 推荐:尽量用
__set_name__获取属性名,避免硬编码。 - 生产级:结合
weakref实现内存友好缓存描述符。 - 与 Pydantic / dataclasses 结合:Pydantic v2 的字段验证底层也大量使用描述符。
- 性能:描述符调用有轻微开销(比普通属性慢 ~10-20%),但在 ORM 场景收益远大于开销。
- 避免误用:不要在实例上动态赋值描述符(无效);多继承时注意描述符冲突。
一句话总结(2026 年认知):
“描述符是 Python ‘属性访问’ 这件事的真正底层钩子。ORM 框架作者必学,业务开发者懂了就能秒懂 SQLAlchemy/Django 的魔法。”
你现在最想深入哪一块?
- 手写一个完整的 ORM 风格描述符(支持延迟加载 + 验证 + 事件)?
- SQLAlchemy 2.0 InstrumentedAttribute 源码级解析?
- Django Field 描述符 vs SQLAlchemy 对比?
- 与
__getattribute__、__getattr__的底层配合细节?
告诉我,我继续给你更针对性的代码和原理图解!