Python 元编程:getattr 与 setattr 方法
在 Python 中,有一种编程技巧让你能在运行时动态修改对象的行为,这就是元编程。而 __getattr__ 和 __setattr__ 是元编程中最常用、最实用的两个魔法方法。掌握它们,你就能写出更灵活、更优雅的代码。
这两个方法到底是干什么的
当你想访问一个对象不存在的属性时,Python 默认会抛出 AttributeError。但如果你定义了 __getattr__ 方法,Python 就会调用它,而不是报错。同样的道理,当你想给一个属性赋值时,__setattr__ 会在赋值之前拦截这次操作。
简单来说:
__getattr__负责处理「读」的操作__setattr__负责处理「写」的操作
这两个方法让你可以在属性访问和赋值时插入自定义逻辑,实现诸如懒加载、属性验证、动态代理等功能。
getattr:优雅地处理缺失属性
基本语法
def __getattr__(self, name):
# 当访问 self.name 且该属性不存在时调用
# 必须返回某个值或抛出 AttributeError
pass
关键点:只有当属性不存在时,这个方法才会被调用。如果你显式定义了一个属性,这个方法不会触发。
实用案例:懒加载数据库连接
class DatabaseManager:
def __init__(self):
self._connections = {}
def __getattr__(self, name):
if name.startswith('conn_'):
db_name = name[5:]
if db_name not in self._connections:
# 第一次访问时才建立连接
self._connections[db_name] = f"连接:{db_name}"
return self._connections[db_name]
raise AttributeError(f"'{self.__class__.__name__}' 对象没有属性 '{name}'")
# 使用示例
db = DatabaseManager()
print(db.conn_mysql) # 输出:连接:mysql(自动创建)
print(db.conn_mysql) # 直接返回已有连接(复用)
在这个例子中,conn_mysql 属性并不存在于对象中,但通过 __getattr__,我们实现了按需创建连接池的效果。
实用案例:动态属性映射
class APIResponse:
def __init__(self, data):
self._raw_data = data
def __getattr__(self, name):
if name in self._raw_data:
return self._raw_data[name]
return None
# 扁平化访问嵌套数据
response = APIResponse({"user": {"name": "张三", "age": 28}})
print(response.user) # 输出:{'name': '张三', 'age': 28}
print(response.user_name) # 输出:张三(自动嵌套访问)
setattr:掌控属性赋值全过程
基本语法
def __setattr__(self, name, value):
# 当给 self.name 赋值时调用
# 通常需要在最后执行实际的赋值操作
self.__dict__[name] = value
关键点:在 __setattr__ 中直接写 self.name = value 会导致递归调用。你需要通过 self.__dict__[name] = value 来绕过递归。
实用案例:属性类型检查
class Config:
def __init__(self):
# 使用 __dict__ 绕过 __setattr__ 的拦截
self.__dict__.update({
'_attrs': {'port': int, 'host': str, 'debug': bool}
})
def __setattr__(self, name, value):
if name in self._attrs:
expected_type = self._attrs[name]
if not isinstance(value, expected_type):
raise TypeError(f"{name} 必须是 {expected_type.__name__} 类型")
self.__dict__[name] = value
# 使用示例
config = Config()
config.port = 8080 # 正常
config.debug = True # 正常
config.port = "abc" # 抛出 TypeError
实用案例:只读属性与不可变对象
class ImmutablePoint:
def __init__(self, x, y):
self.__dict__['_values'] = {'x': x, 'y': y}
def __setattr__(self, name, value):
if hasattr(self, '_values'):
if name in self._values:
raise AttributeError(f"'{self.__class__.__name__}' 的属性是只读的")
self.__dict__[name] = value
def __getattr__(self, name):
if name in self._values:
return self._values[name]
raise AttributeError(f"'{self.__class__.__name__}' 对象没有属性 '{name}'")
p = ImmutablePoint(3, 4)
print(p.x, p.y) # 输出:3 4
p.x = 5 # 抛出 AttributeError
两者配合:实现完整的属性拦截
class ObservedObject:
def __init__(self):
self.__dict__['_changes'] = []
self.__dict__['_data'] = {}
def __getattr__(self, name):
if name.startswith('_'):
raise AttributeError(f"私有属性 '{name}' 不可访问")
return self._data.get(name, None)
def __setattr__(self, name, value):
if name.startswith('_'):
self.__dict__[name] = value
return
old_value = self._data.get(name, '<未定义>')
self._changes.append(f"'{name}' 从 {old_value} 变为 {value}")
self._data[name] = value
# 使用示例
obj = ObservedObject()
obj.name = "初始值"
obj.name = "修改后"
print(obj._changes) # 输出:["'name' 从 <未定义> 变为 初始值", "'name' 从 初始值 变为 修改后"]
这个模式常用于数据绑定、状态追踪、ORM 框架等场景。
重要注意事项
递归陷阱:在 __setattr__ 中,绝对不要使用 self.name = value 或 self.__dict__['name'] = value 以外的赋值方式,否则会导致无限递归。
性能考量:这两个方法会在每次属性访问时被调用,如果你的类需要频繁访问大量属性,使用这两个方法会带来额外的开销。评估是否真的需要这种灵活性,再决定是否使用。
异常处理:__getattr__ 方法中,如果你不希望拦截某个属性,必须抛出 AttributeError。返回 None 或其他值会静默吞掉这个异常,可能导致难以调试的问题。
与 __getattribute__ 的区别:__getattribute__ 会在所有属性访问时调用(包括已存在的属性),而 __getattr__ 只在属性不存在时调用。后者更安全、更可控。

暂无评论,快来抢沙发吧!