Python 为我们提供了三个魔法方法,让我们可以在读取和设置对象属性时插入自定义逻辑,就像在属性访问的必经之路上布下“钩子”。这三个方法分工不同,理解它们的调用时机和区别,是写出灵活且健壮代码的关键。
getattr —— 查不到时才调用
- 触发时机:当你访问一个不存在的属性时,Python 才会调用
getattr。 - 方法签名:
getattr(self, name),name是属性名字符串。 - 核心作用:提供“默认值”或“动态属性”的机制,避免
AttributeError。
实用示例:创建一个对象,访问未定义的属性时返回 None 而不是报错。
class SafeObject:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def __getattr__(self, name):
return None
obj = SafeObject(x=1, y=2)
print(obj.x) # 1
print(obj.z) # None,不会报错
print(obj.abc) # None
典型应用场景:配置对象的默认值、代理模式的转发(如将未知属性转发给内部对象)。
getattribute —— 不管存不存在都会被调用
- 触发时机:每次访问属性时都会被调用,无论该属性是否存在。这在属性访问流程中是最顶层的拦截点。
- 方法签名:
getattribute(self, name) - 风险警告:在
getattribute内部很容易因为不小心再次触发属性访问而导致无限递归。通常需要借助object.getattribute(self, name)来获取真正的属性值。
实用示例:记录所有属性访问日志。
class LoggedAccess:
def __getattribute__(self, name):
print(f"正在访问属性: {name}")
# 必须调用基类的 __getattribute__,避免递归
return super().__getattribute__(name)
obj = LoggedAccess()
obj.x = 10
print(obj.x)
# 输出:
# 正在访问属性: x
# 10
重要提醒:覆盖 getattribute 是高级技巧,大多数情况应优先考虑 getattr 或 @property。一旦使用,务必记住任何属性访问(包括 self.dict)都会触发它,因此要小心处理。
setattr —— 设置属性时调用
- 触发时机:每次给属性赋值时(如
self.name = value)都会调用,无论属性之前是否存在。 - 方法签名:
setattr(self, name, value) - 核心作用:拦截属性赋值操作,实现校验、类型检查、不可变性控制等。
实用示例:创建一个只读对象,禁止后续修改属性。
class ReadOnly:
def __init__(self, **kwargs):
# 绕过 __setattr__ 直接写入实例字典
self.__dict__.update(kwargs)
def __setattr__(self, name, value):
raise AttributeError("该对象是只读的,不能修改属性")
obj = ReadOnly(x=5)
print(obj.x) # 5
obj.x = 10 # 抛出 AttributeError
另一个常见场景:类型强制转换,比如保证某个属性总是整数:
class StrictType:
def __setattr__(self, name, value):
if name == 'age' and not isinstance(value, int):
raise TypeError("age 必须是整数")
self.__dict__[name] = value
obj = StrictType()
obj.age = 30 # OK
obj.age = '老' # 抛出 TypeError
注意:在 setattr 内部,必须使用 self.dict[name] = value 来实际存储值,而不能使用 self.name = value,否则会无限递归调用自身。
调用顺序与关系总结
- 读取属性时:
getattribute总是最先被调用。如果它找不到属性(即需要抛出AttributeError),然后才会轮到getattr作为最后的“兜底”尝试。 - 设置属性时:
setattr总是被调用,不论属性是否已存在。 - 最佳实践:
- 一般优先使用
@property装饰器来管理单个属性的访问控制,既清晰又安全。 - 当需要对多个或全部属性做统一拦截时,再考虑
getattr/setattr。 getattribute尽量不要乱动,除非你非常清楚自己在做什么。
掌握这三个方法,可以让你设计出更灵活的对象模型,比如动态代理、惰性加载属性、属性校验容器等,在框架开发和底层库编写中尤其有用。