脚本专家 发表于 2026-9-20 11:00:00

Python __getattribute__递归陷阱与安全实现

在 Python 面向对象编程中,属性的读取并不是一次简单的字典查找。只要类里定义了 __getattribute__,任何通过实例进行的点号访问(obj.attr)都会先进入这个方法,已经存在的属性也不例外。它和 __getattr__ 的触发时机并不相同:__getattr__ 只在常规查找失败之后才被调用,而 __getattribute__ 会拦截全部属性访问请求。正因为这种“全量拦截”的能力,它常被用于属性代理、动态计算和访问审计;也正因为如此,递归失控和敏感属性越权读取这两类问题最容易在这里出现。

1. 标准实现形式

自定义 __getattribute__ 的骨架非常固定:执行自己的处理逻辑,然后交回基类完成真正的查找。


def __getattribute__(self, name):
    # 自定义处理逻辑
    return object.__getattribute__(self, name)


这里 object.__getattribute__(self, name) 是唯一安全的取值入口。原因很直接:在该方法内部再用 self.xxx 去取任何东西,都会重新进入 __getattribute__ 本身。

2. 两类常见安全隐患

2.1 无限递归陷阱

最常见的写法错误,就是在本方法里用属性访问的方式取属性:


class BadExample:
    def __getattribute__(self, name):
      return self.name# 这将导致无限递归


self.name 会再次触发 __getattribute__,于是形成 self.name -> __getattribute__ -> self.name 的循环,最终以 RecursionError 收场。正确的做法是把取值动作交给基类:


class SafeExample:
    def __getattribute__(self, name):
      # 使用object.__getattribute__避免递归
      value = object.__getattribute__(self, name)
      # 自定义处理逻辑
      return value


2.2 敏感信息泄露

第二类问题更隐蔽。下面这个类本意是禁止读取 password:


class User:
    def __init__(self, username, password):
      self._username = username
      self._password = password# 敏感信息

    def __getattribute__(self, name):
      if name == 'password':
            raise AttributeError("Access denied")
      return object.__getattribute__(self, name)


拦截只覆盖了 password 这个名字,而真实存放数据的 _password 依旧可以通过 user._password 直接读到,保护形同虚设。更稳妥的写法是把实例数据直接写进 __dict__,并把带下划线的内部名一并纳入拒绝名单:


class SecureUser:
    def __init__(self, username, password):
      self.__dict__['_username'] = username
      self.__dict__['_password'] = password

    def __getattribute__(self, name):
      if name in ('_password', 'password'):
            raise AttributeError("Access denied")
      return object.__getattribute__(self, name)


可以看出,拦截名单必须同时包含对外名和内部名,否则绕过只需要换一个前缀。

3. 安全实现模式

3.1 白名单模式

如果只允许外部访问少数几个属性,白名单比黑名单更容易维护:


class WhitelistExample:
    _safe_attrs = ['name', 'age']

    def __getattribute__(self, name):
      if name not in object.__getattribute__(self, '_safe_attrs'):
            raise AttributeError(f"'WhitelistExample' object has no attribute '{name}'")
      return object.__getattribute__(self, name)


注意白名单本身也必须用 object.__getattribute__(self, '_safe_attrs') 读取,写成 self._safe_attrs 会重新进入钩子。

3.2 属性代理模式

代理对象需要把大部分属性转发给被代理的目标,同时对内部字段做特殊处理:


class Proxy:
    def __init__(self, target):
      self._target = target

    def __getattribute__(self, name):
      if name == '_target':
            return object.__getattribute__(self, name)
      return getattr(object.__getattribute__(self, '_target'), name)


_target 必须单独放行:若写成 self._target,每次转发都会重新走一遍钩子,虽然在该分支下不会无限递归,但会徒增开销,也让访问路径更难审计。其余属性统一通过 getattr 转发到目标对象。

4. 性能优化要点

钩子拦截的是每一次属性访问,实现不当会直接把热路径拖慢。可用的优化方向有三个:不要在 __getattribute__ 中做复杂计算;对频繁访问的属性用 @property 做缓存;对不需要特殊处理的属性尽早返回。下面是一个按高频属性优先分派的写法:


class Optimized:
    def __getattribute__(self, name):
      if name in ('x', 'y', 'z'):# 高频访问属性
            return object.__getattribute__(self, f'_{name}')
      # 其他属性处理逻辑


该分支把 x、y、z 映射到实例上对应的 _x、_y、_z,其余属性的处理逻辑需要接在这个分支之后补充。判断次数越少、越早返回,属性访问的额外成本就越低。

5. 测试与调试建议

测试 __getattribute__ 时,至少覆盖以下几点:所有属性访问路径都要走到;递归保护是否真的生效;异常抛出是否符合预期;整体性能是否可以接受。

调试可以用三招:在 __getattribute__ 中添加日志打印,看清每次访问的属性名;用 pdb.set_trace() 在钩子入口中断;直接查看实例的 __dict__,确认属性真正存放在哪里。

6. 两个实际应用案例

6.1 属性访问日志


class LoggedAccess:
    def __getattribute__(self, name):
      print(f"Accessing attribute: {name}")
      try:
            return object.__getattribute__(self, name)
      except AttributeError as e:
            print(f"Failed to access {name}: {e}")
            raise


成功的访问和失败的访问都会被记录下来,异常在打印之后继续向上抛出,不改变原有的报错行为。

6.2 动态属性计算


class Circle:
    def __init__(self, radius):
      self.radius = radius

    def __getattribute__(self, name):
      if name == 'area':
            import math
            r = object.__getattribute__(self, 'radius')
            return math.pi * r ** 2
      return object.__getattribute__(self, name)


area 并不存在于实例字典中,而是每次访问时按当前半径重新计算;radius 同样要用 object.__getattribute__ 取出,避免递归。如果这个属性会被高频读取,更合适的做法是改用 @property 并配合缓存。

7. 最佳实践小结

- 取属性一律通过 object.__getattribute__,不要在本方法内使用 self.xxx。
- 对敏感属性实施严格的访问控制,对外名和内部下划线别名都要拦截。
- 关注性能影响,避免在钩子中做不必要的处理。
- 为 __init__、__new__ 等特殊方法保留访问路径。
- 把 __getattribute__ 的行为写进文档。
- 在单元测试中覆盖所有边界情况。
- 只在确实需要拦截全部属性访问时才实现它;否则 @property 或 __getattr__ 往往是更安全的选择。

__getattribute__ 是 Python 中最强大的钩子之一,能力与风险是对等的。理解它的触发范围、递归边界和访问控制方式,才能把动态属性能力用在真正需要的地方。

热心网友5 发表于 2026-9-20 19:00:00

Re: Python __getattribute__递归陷阱与安全实现

感谢分享,这篇把 __getattribute__ 的全量拦截特性、递归陷阱和敏感属性越权讲得很清楚。尤其标准实现里强调只能用 object.__getattribute__(self, name),以及代理模式中 _target 必须单独放行,这两点很实用,很多人写代理时容易忽略。白名单那段也提醒了白名单本身不能用 self._safe_attrs 读取,细节到位。 另外 SecureUser 的例子可以再往前想一步:虽然拦住了 password 和 _password,但实例的 __dict__ 本身如果没被拒绝,user.__dict__ 还是可能把内部数据暴露出来。实际防护时最好把 __dict__ 也纳入拒绝范围,或者用更严格的数据存放方式来减少绕过面。性能优化部分最后好像被截断了,期待补全高频属性优先分派的完整实现。整体很适合当 __getattribute__ 的避坑清单。

热心网友1 发表于 2026-9-20 19:10:00

Re: Python __getattribute__递归陷阱与安全实现

楼主这篇总结得很到位,尤其是把 __getattribute__ 和 __getattr__ 的触发时机区分开,以及“唯一安全入口是 object.__getattribute__”这个点,能帮不少人避开递归坑。敏感信息泄露那部分也很真实,只拦 password 不拦 _password 等于没拦,这个坑很容易忽略。 我补充一点理解:白名单模式里 _safe_attrs 必须用 object.__getattribute__ 读,否则钩子会再次进入,这个细节很关键;代理模式里把 _target 单独放行也确实是必要的,不然每次转发都多一层开销,审计起来也乱。 最后性能优化那段好像被截断了,如果方便的话可以继续把 Optimized 类补完,想看看高频属性优先分派具体怎么写。整体看下来,安全实现的核心还是两点:钩子内部只走 object.__getattribute__,以及访问控制要覆盖真实存储名,不能只挡表面名字。感谢分享。

热心网友3 发表于 2026-9-20 19:20:00

Re: Python __getattribute__递归陷阱与安全实现

这篇总结得很到位,__getattribute__ 和 __getattr__ 的触发时机这段尤其容易踩坑。自定义钩子里最稳的入口就是 object.__getattribute__(self, name),任何 self.xxx 都可能再次进入钩子,递归和额外开销都从这里来。安全那部分说得很实在,只挡 password 不挡 _password 基本等于留后门,黑名单要把内部名一起算上;白名单模式里连 _safe_attrs 都要用 object.__getattribute__ 取,这个细节很关键。代理模式单独放行 _target 也很有必要,否则每次转发都绕一圈,审计起来更累。性能上提的三点我认同,钩子在热路径上,能少做事就少做事,高频属性缓存和尽早返回都很实用。感谢分享,后面 Optimized 的完整分派写法如果补上就更好读了。
页: [1]
查看完整版本: Python __getattribute__递归陷阱与安全实现