Python中的属性拦截与属性管理(`__getattr__`、`__getattribute__`、`__setattr__`、`__delattr__`)深入解析
字数 919 2025-11-08 10:03:28
Python中的属性拦截与属性管理(__getattr__、__getattribute__、__setattr__、__delattr__)深入解析
属性拦截是Python面向对象编程中的高级特性,允许你在访问、设置或删除对象属性时插入自定义逻辑。我们将通过一个实际场景来理解这些方法的区别、执行顺序和常见陷阱。
场景设定:创建一个智能字典类,它能记录属性访问日志,并对不存在的属性返回默认值而不是抛出异常。
1. 基础方法区分
__getattr__(self, name):仅当正常属性查找失败时调用(即实例字典、类、父类中均不存在该属性)__getattribute__(self, name):无条件调用所有属性访问的入口(包括特殊方法)__setattr__(self, name, value):拦截所有属性赋值操作__delattr__(self, name):拦截所有属性删除操作
2. 实现基础日志功能
class LoggingDict:
def __getattr__(self, name):
print(f"__getattr__被调用,属性名: {name}")
return f"默认值_{name}" # 给不存在的属性返回默认值
def __setattr__(self, name, value):
print(f"__setattr__被调用: {name} = {value}")
super().__setattr__(name, value) # 必须调用父类避免递归
d = LoggingDict()
d.existing = "实际值"
print(d.existing) # 正常访问,不会触发__getattr__
print(d.non_existing) # 触发__getattr__
输出结果:
__setattr__被调用: existing = 实际值
实际值
__getattr__被调用,属性名: non_existing
默认值_non_existing
3. __getattribute__的深度介入
现在添加__getattribute__并观察执行顺序:
class VerboseLoggingDict(LoggingDict):
def __getattribute__(self, name):
print(f"__getattribute__被调用: {name}")
return super().__getattribute__(name) # 必须通过父类继续查找链
v = VerboseLoggingDict()
v.test = "测试值"
print("--- 访问存在的属性 ---")
print(v.test)
print("--- 访问不存在的属性 ---")
print(v.unknown)
输出结果:
__getattribute__被调用: __dict__
__setattr__被调用: test = 测试值
--- 访问存在的属性 ---
__getattribute__被调用: test
测试值
--- 访问不存在的属性 ---
__getattribute__被调用: unknown
__getattr__被调用,属性名: unknown
默认值_unknown
4. 关键陷阱:避免递归
在__getattribute__或__setattr__中直接访问属性会引发无限递归:
class BrokenDict:
def __getattribute__(self, name):
return self.name # 错误!再次触发__getattribute__
# 正确做法:始终通过super()或直接操作__dict__
class CorrectDict:
def __getattribute__(self, name):
# 方法1:通过父类访问
return super().__getattribute__(name)
def __setattr__(self, name, value):
# 方法2:直接操作对象字典
self.__dict__[name] = value
5. 实际应用:惰性属性加载
结合这些方法实现首次访问时计算的属性:
class LazyProperty:
def __init__(self):
self._cache = {}
def __getattr__(self, name):
if name not in self._cache:
print(f"计算属性 {name} 的值")
self._cache[name] = f"{name}_计算结果"
return self._cache[name]
lazy = LazyProperty()
print(lazy.data) # 第一次访问触发计算
print(lazy.data) # 直接从缓存返回
6. 方法调用顺序总结
- 访问
obj.attr时,首先调用__getattribute__ - 如果属性不存在,
__getattribute__抛出AttributeError - 解释器捕获错误后尝试调用
__getattr__ - 若
__getattr__未定义或再次抛出异常,则向用户抛出AttributeError
关键要点:
__getattribute__总在__getattr__之前调用- 在
__getattribute__中避免直接访问实例属性 - 通过
super()或object.__getattribute__()打破递归链 __getattr__更适合实现"后备属性"或虚拟属性