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。
完整属性查找流程(读取)
- 调用
obj.__getattribute__('name')(默认继承自object) - 检查是否为数据描述符(实现了
__set__的描述符),是则调用其__get__ - 查找实例自身的
__dict__(或__slots__槽位) - 查找类的
__dict__,遇到非数据描述符则调用其__get__ - 沿 MRO 继承链向上遍历父类,重复上一步
- 全部失败,调用
obj.__getattr__('name')(若定义) - 仍未定义则抛出
AttributeError
数据描述符优先于实例字典、非数据描述符次于实例字典,是描述符 descriptor的核心优先级规则;实例字典与槽位的存储机制见类属性与实例属性。拦截这套流程的更底层入口是元类 metaclass——类的创建本身。