进阶篇 · Advanced
lv.3 进阶
kp-027
元编程(属性拦截、描述符、元类)
1. 一句话定义
元编程 = 编写操纵代码的代码:拦截属性访问(__getattr__/__getattribute__)、把"属性"变成带行为的对象(描述符)、或在类被创建的那一刻改造类(__init_subclass__、元类)。
2. 为什么重要
ORM 字段、property、dataclass、pytest 的魔法,全部建立在这三层之上。目标不是天天写它,而是读懂它——这是从"用框架"到"理解框架"的门票。
3. 前置知识
kp-017(属性即 dict)、kp-019(协议派发)。
4. 核心概念(三层递进)
① 属性拦截:
python
class Lazy:
def __getattr__(self, name): # 只在"常规查找失败后"触发(安全网)
if name.startswith("get_"):
return lambda: f"计算 {name[4:]}"
raise AttributeError(name)
def __getattribute__(self, name): # 所有属性访问都过它(慎用,易死循环)
...② 描述符(__set_name__/__get__/__set__)——"把属性变成对象":
python
class Positive:
def __set_name__(self, owner, name): self.name = name # 3.6+:自动知道字段名
def __get__(self, obj, objtype=None):
return obj.__dict__[self.name]
def __set__(self, obj, value):
if value <= 0: raise ValueError(f"{self.name} 必须为正")
obj.__dict__[self.name] = value
class Order:
qty = Positive() # qty 永远 > 0 —— 一个类复用的验证器③ 类创建钩子与元类:
python
class Registry:
def __init_subclass__(cls, key=None, **kw): # 每有子类定义即回调(现代首选)
super().__init_subclass__(**kw)
if key: PLUGINS[key] = cls5. 原理与机制
text
obj.x 的完整旅程:
type(obj).__getattribute__(obj, "x")
→ 数据描述符(类里定义了 __set__ 的)?→ 描述符 __get__
→ obj.__dict__["x"]?→ 直接返回
→ 类属性(可能是非数据描述符/普通值)→ 命中即返回
→ __getattr__(obj, "x") ← 最后的安全网
→ AttributeErrorproperty 就是内置的数据描述符;函数本身也是描述符(__get__ 绑定 self——kp-017 的"方法绑定"机制层)。元类(class Foo(metaclass=...))在类语句执行时接管"类的类",如今多数场景已被 __init_subclass__ + 类装饰器取代,仅在框架级元编程中出现。
6. 关键事实(模型/图示)
text
元编程三层(由浅到深):
属性拦截 → 动态行为(代理、懒加载、__getattr__ 转发)
描述符 → 复用的属性行为(property/验证/ORM 字段)
类创建钩子/元类 → 类的自动注册与改造(插件系统、ABC)7. 直观类比
描述符像物业统一管理的门禁:obj.qty 看似是"直接开门",实际每次都过门禁(__get__/__set__),门禁记录了这扇门是谁家的(__set_name__)、谁都能进出但必须刷卡(验证)。__getattr__ 是前台总机:常规分机都打不通(查无此人)才转总机;__getattribute__ 则是把总机装在所有电话线路上——线路被劫持,任何呼叫都先过它。
8. 实例与案例
python
# 用描述符读懂 Django/SQLAlchemy 风格的字段
class Field:
def __set_name__(self, owner, name): self.name = name
def __get__(self, obj, objtype=None):
return obj.__dict__.get(self.name)
def __set__(self, obj, v):
obj.__dict__[self.name] = v
class User:
name = Field()
email = Field()
u = User(); u.name = "蛇" # 看似普通属性,实际走描述符验证/钩子链9. 常见误区
- 在
__getattribute__里访问self.x→ 无限递归;必须经super().__getattribute__(name)。 - 混淆
__getattr__(兜底)与__getattribute__(全拦截) —— 90% 的动态需求用前者。 - 描述符写在实例上 —— 必须是类属性才生效;写在
__init__里self.qty = Positive()无效。 - 上来就学元类 —— 2020 年代的教学顺序应是:拦截 → 描述符 →
__init_subclass__,元类放到"确实需要"时再学。
10. 自测题
obj.x未定义时,__getattr__与__getattribute__各在什么时机被调用?- 为什么
property能拦截赋值?它的本质是什么? __init_subclass__能替代元类的哪些典型用途?
参考答案
__getattribute__是所有属性访问的第一站;__getattr__只在常规查找全部失败后作为兜底调用。- property 是内置的数据描述符(定义了
__get__/__set__),读写都经过它。 - 子类自动注册、按声明改写子类(加方法/校验字段)、单例/缓存类行为——绝大多数"类创建时自动处理"场景。
11. 与其他知识点的关系
12. 延伸阅读
- Python 数据模型:Customizing attribute access / Descriptors
- PEP 487(
__set_name__与__init_subclass__)