Python `__getattr__` 与 `__getattribute__` 区别与应用场景

好的,各位观众,欢迎来到今天的 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'

可以看到,只有当 agecity 属性不存在时,__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__。下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注