好的,各位观众,欢迎来到今天的 Python 魔法世界!我是你们的魔法师,今天我们要聊聊 Python 中两个非常神秘,但又非常强大的函数:__getattr__
和 __getattribute__
。
这两个家伙经常被混淆,搞得很多初学者头大。今天,我就用最通俗易懂的方式,加上大量的例子,帮你彻底搞懂它们,让你在 Python 的魔法世界里更加游刃有余。
开场白:两个“取经人”的故事
想象一下,我们要去西天取经。__getattribute__
就像孙悟空,它总是第一个冲出去,不管有没有妖怪,它都要先探探路。而 __getattr__
就像猪八戒,只有孙悟空说“师傅,前面没路了!”,它才会慢吞吞地出来看看,是不是真的没有宝贝可以捞了。
这就是它们最核心的区别:__getattribute__
永远是第一个被调用的,而 __getattr__
只有在属性查找失败时才会被调用。
第一幕:__getattribute__
– “我先来!”
__getattribute__
是一个很霸道的家伙。只要你试图访问一个对象的属性,不管这个属性存不存在,它都会被调用。
class MyClass:
def __init__(self, name):
self.name = name
def __getattribute__(self, name):
print(f"__getattribute__ called for: {name}")
return super().__getattribute__(name)
obj = MyClass("Alice")
print(obj.name)
print(obj.age) # 这里也会调用 __getattribute__
运行结果:
__getattribute__ called for: name
Alice
__getattribute__ called for: age
Traceback (most recent call last):
File "<stdin>", line 10, in <module>
File "<stdin>", line 6, in __getattribute__
AttributeError: 'MyClass' object has no attribute 'age'
注意看,即使 age
这个属性不存在,__getattribute__
仍然被调用了。而且,如果没有 super().__getattribute__(name)
,你会发现你根本无法访问任何属性,因为 __getattribute__
把所有的访问都拦截了。
重点:super().__getattribute__(name)
在 __getattribute__
中,一定要调用 super().__getattribute__(name)
,否则你会陷入无限递归的深渊!
为什么呢?因为如果你直接使用 self.name
,它又会触发 __getattribute__
,然后 __getattribute__
又调用 self.name
,就这样无限循环下去,直到程序崩溃。
super()
的作用是调用父类的 __getattribute__
方法,避免无限递归。
应用场景:日志记录、权限控制
__getattribute__
非常适合用来做一些全局性的操作,比如:
- 日志记录: 记录每次属性访问,方便调试。
- 权限控制: 检查用户是否有权限访问某个属性。
- 延迟加载: 在第一次访问属性时才进行加载。
class LoggedClass:
def __getattribute__(self, name):
print(f"Accessing attribute: {name}")
return super().__getattribute__(name)
class SecuredClass:
def __getattribute__(self, name):
if name.startswith('_'): # 阻止访问私有属性
raise AttributeError("Cannot access private attributes")
return super().__getattribute__(name)
class LazyLoadedClass:
def __init__(self):
self._data = None
def __getattribute__(self, name):
if name == 'data':
if self._data is None:
print("Loading data...")
self._data = "Some expensive data" # 模拟加载数据
return self._data
return super().__getattribute__(name)
# 使用示例
logged_obj = LoggedClass()
logged_obj.some_attribute = "Hello"
print(logged_obj.some_attribute)
secured_obj = SecuredClass()
secured_obj.public_attribute = "World"
print(secured_obj.public_attribute)
try:
print(secured_obj._private_attribute) # 会抛出 AttributeError
except AttributeError as e:
print(e)
lazy_obj = LazyLoadedClass()
print(lazy_obj.data) # 第一次访问会加载数据
print(lazy_obj.data) # 第二次直接返回数据
第二幕:__getattr__
– “没找到,我来试试!”
__getattr__
是一个比较懒的家伙。只有当 Python 找不到指定的属性时,它才会出动。
class MyClass:
def __init__(self, name):
self.name = name
def __getattr__(self, name):
print(f"__getattr__ called for: {name}")
if name == 'age':
return 18 # 如果访问 age 属性,返回 18
else:
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
obj = MyClass("Bob")
print(obj.name)
print(obj.age)
print(obj.city)
运行结果:
Bob
__getattr__ called for: age
18
__getattr__ called for: city
Traceback (most recent call last):
File "<stdin>", line 13, in <module>
File "<stdin>", line 9, in __getattr__
AttributeError: 'MyClass' object has no attribute 'city'
可以看到,只有当 age
和 city
属性不存在时,__getattr__
才会被调用。而且,如果 __getattr__
无法处理这个属性,它应该抛出一个 AttributeError
异常,告诉调用者这个属性确实不存在。
应用场景:动态属性、代理模式
__getattr__
非常适合用来实现一些动态的特性,比如:
- 动态属性: 根据属性名动态生成属性值。
- 代理模式: 将属性访问转发给另一个对象。
- 简化接口: 将多个属性组合成一个更方便的属性。
class DynamicAttributes:
def __getattr__(self, name):
if name.startswith('calculate_'):
value = int(name.split('_')[1])
return value * 2 # 动态计算属性值
else:
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
class Proxy:
def __init__(self, obj):
self._obj = obj
def __getattr__(self, name):
print(f"Proxying attribute access for: {name}")
return getattr(self._obj, name) # 将属性访问转发给被代理对象
class SimplifiedInterface:
def __init__(self, x, y):
self.x = x
self.y = y
def __getattr__(self, name):
if name == 'sum':
return self.x + self.y # 简化接口,将 x + y 封装成 sum 属性
else:
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
# 使用示例
dynamic_obj = DynamicAttributes()
print(dynamic_obj.calculate_10)
real_obj = "Hello, World!"
proxy_obj = Proxy(real_obj)
print(proxy_obj.upper())
simplified_obj = SimplifiedInterface(5, 3)
print(simplified_obj.sum)
第三幕:__getattribute__
和 __getattr__
的爱恨情仇
现在,我们来总结一下这两个家伙的区别:
特性 | __getattribute__ |
__getattr__ |
---|---|---|
调用时机 | 每次访问属性都会被调用 | 只有在属性查找失败时才会被调用 |
作用 | 拦截所有属性访问 | 处理不存在的属性 |
使用场景 | 日志记录、权限控制、延迟加载 | 动态属性、代理模式、简化接口 |
注意事项 | 必须调用 super().__getattribute__(name) ,防止无限递归 |
应该抛出 AttributeError 异常,表示属性确实不存在 |
一个更复杂的例子:结合使用
class SmartClass:
def __init__(self, data):
self._data = data
def __getattribute__(self, name):
print(f"__getattribute__ called for: {name}")
if name.startswith('cached_'):
cache_name = '_' + name
if hasattr(self, cache_name):
print(f"Returning cached value for: {name}")
return getattr(self, cache_name)
else:
print(f"Calculating and caching value for: {name}")
result = self._calculate(name[7:]) # 去掉 "cached_" 前缀
setattr(self, cache_name, result)
return result
return super().__getattribute__(name)
def __getattr__(self, name):
print(f"__getattr__ called for: {name}")
if name == 'transformed_data':
return self._data.upper() # 动态生成 transformed_data 属性
else:
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
def _calculate(self, key):
# 模拟一个耗时的计算
print(f"Calculating value for key: {key}")
return len(self._data) * len(key)
# 使用示例
obj = SmartClass("hello")
print(obj.cached_value1) # 第一次访问,会计算并缓存
print(obj.cached_value1) # 第二次访问,直接返回缓存
print(obj.transformed_data) # 动态生成 transformed_data 属性
print(obj.nonexistent_attribute) # 抛出 AttributeError
在这个例子中,__getattribute__
负责拦截以 cached_
开头的属性访问,如果属性已经缓存,就直接返回缓存值,否则就调用 _calculate
方法计算并缓存。__getattr__
负责动态生成 transformed_data
属性。
总结:掌握魔法,创造奇迹
__getattr__
和 __getattribute__
是 Python 中非常强大的魔法,掌握它们,你就可以创造出各种各样神奇的效果。
__getattribute__
适合做一些全局性的拦截和处理。__getattr__
适合处理不存在的属性,实现动态特性。
但是,记住,能力越大,责任越大。过度使用这两个魔法可能会导致代码难以理解和维护。所以,请谨慎使用,并添加适当的注释,让你的代码更加清晰易懂。
希望今天的讲座能帮助你更好地理解 __getattr__
和 __getattribute__
。下次再见!