>>>PyPathPython 学习站
首页›进阶篇›kp-027
进阶篇 · Advanced lv.3 进阶 kp-027

元编程(属性拦截、描述符、元类)

前置知识:kp-017、kp-019

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] = cls

5. 原理与机制

text
obj.x 的完整旅程:
  type(obj).__getattribute__(obj, "x")
    → 数据描述符(类里定义了 __set__ 的)?→ 描述符 __get__
    → obj.__dict__["x"]?→ 直接返回
    → 类属性(可能是非数据描述符/普通值)→ 命中即返回
    → __getattr__(obj, "x")  ← 最后的安全网
    → AttributeError

property 就是内置的数据描述符;函数本身也是描述符(__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. 常见误区

  1. 在 __getattribute__ 里访问 self.x → 无限递归;必须经 super().__getattribute__(name)。
  2. 混淆 __getattr__(兜底)与 __getattribute__(全拦截) —— 90% 的动态需求用前者。
  3. 描述符写在实例上 —— 必须是类属性才生效;写在 __init__ 里 self.qty = Positive() 无效。
  4. 上来就学元类 —— 2020 年代的教学顺序应是:拦截 → 描述符 → __init_subclass__,元类放到"确实需要"时再学。

10. 自测题

  1. obj.x 未定义时,__getattr__ 与 __getattribute__ 各在什么时机被调用?
  2. 为什么 property 能拦截赋值?它的本质是什么?
  3. __init_subclass__ 能替代元类的哪些典型用途?
参考答案
  1. __getattribute__ 是所有属性访问的第一站;__getattr__ 只在常规查找全部失败后作为兜底调用。
  2. property 是内置的数据描述符(定义了 __get__/__set__),读写都经过它。
  3. 子类自动注册、按声明改写子类(加方法/校验字段)、单例/缓存类行为——绝大多数"类创建时自动处理"场景。

11. 与其他知识点的关系

  • kp-020 dataclass:元编程的"官方产品化"。
  • kp-024 装饰器:类装饰器与 __init_subclass__ 常互为替代。
  • kp-018 MRO:属性查找的路线图。

12. 延伸阅读

  • Python 数据模型:Customizing attribute access / Descriptors
  • PEP 487(__set_name__ 与 __init_subclass__)