Python 对属性的读取、赋值、删除均有对应的魔法方法拦截:__getattribute__ 是访问总入口,__getattr__ 是查找失败后的兜底,__setattr__/__delattr__ 管写入与删除——它们共同构成元编程的核心基础。

属性读取的两个钩子

__getattribute__:无条件触发的总入口

  • 每次访问 obj.name 都会无条件触发,是属性访问的最高优先级入口
  • 内部禁止直接 self.name 访问属性——会无限递归;必须通过 super().__getattribute__(name) 调用父类默认查找逻辑

__getattr__:兜底回调

  • 仅当完整属性查找链全部失败、属性不存在时才触发
  • 常用于动态属性、代理模式、缺失属性默认值
class Demo:
    def __getattribute__(self, name):
        print(f"访问属性:{name}")
        return super().__getattribute__(name)  # 调用父类默认查找逻辑
 
    def __getattr__(self, name):
        print(f"属性 {name} 不存在,返回默认值")
        return None
钩子触发时机典型用途
__getattribute__(self, name)每次访问 obj.name,无条件拦截/审计所有属性访问
__getattr__(self, name)完整查找链失败后动态属性、代理、默认值
__setattr__(self, name, value)执行 obj.name = value 时校验、只读保护、记录变更
__delattr__(self, name)执行 del obj.name 时禁止删除、清理联动

属性写入与删除

写入与删除各有一个钩子,内部操作属性同样必须使用 super() 避免递归:

class Guard:
    def __setattr__(self, name, value):
        if name == "role" and value not in ("admin", "user"):
            raise ValueError("非法角色")
        super().__setattr__(name, value)  # 不能写 self.name = value,会无限递归
 
    def __delattr__(self, name):
        raise AttributeError(f"{name} 不允许删除")

super().__getattribute__ 的本质

重写 __getattribute__ 后调用 super(),若父类也找不到属性怎么办?关键在于 super() 并非简单获取直接父类:

  • super() 创建一个 MRO 切片代理对象,查找起点是当前类在 MRO 中的下一个类
  • super().__getattribute__(name) 等价于:从 MRO 下一个类开始查找 __getattribute__ 并调用其实现,不会递归调用本类的 __getattribute__
  • 单继承下等价于 object.__getattribute__(self, name);多继承场景下 super() 保证链式调用,是规范写法

MRO 的构造规则详见继承与 MRO。

完整属性查找流程(读取)

  1. 调用 obj.__getattribute__('name')(默认继承自 object)
  2. 检查是否为数据描述符(实现了 __set__ 的描述符),是则调用其 __get__
  3. 查找实例自身的 __dict__(或 __slots__ 槽位)
  4. 查找类的 __dict__,遇到非数据描述符则调用其 __get__
  5. 沿 MRO 继承链向上遍历父类,重复上一步
  6. 全部失败,调用 obj.__getattr__('name')(若定义)
  7. 仍未定义则抛出 AttributeError

数据描述符优先于实例字典、非数据描述符次于实例字典,是描述符 descriptor的核心优先级规则;实例字典与槽位的存储机制见类属性与实例属性。拦截这套流程的更底层入口是元类 metaclass——类的创建本身。