
Python 模块级 __getattr__ 在模块外部生效,但在模块内部无法拦截未定义变量访问;本文提供一种基于重执行代码帧并定制 __builtins__ 的可靠绕过方案。
python 模块级 `__getattr__` 在模块外部生效,但在模块内部无法拦截未定义变量访问;本文提供一种基于重执行代码帧并定制 `__builtins__` 的可靠绕过方案。
在 Python 3.7+ 中,模块可定义 __getattr__(name) 函数,用于动态处理对未声明名称的访问(如 import mymod; mymod.undefined_attr)。但该机制仅作用于模块外的属性访问——当在模块自身作用域中直接使用未定义变量(如 print(a))时,解释器会先查 globals(),再查 __builtins__,而不会触发模块的 __getattr__。
根本原因在于:模块内变量查找走的是 LEGB 规则中的 G(Global)层,即 globals() 字典查找,失败后直接跳转至 __builtins__,完全绕过模块对象的 __getattr__ 钩子。
因此,不能通过包装 globals()(如继承 dict 并重写 __getitem__)来解决——因为 globals() 是只读绑定,且其修改不参与名称解析流程;真正需要干预的是 __builtins__ 的行为。
以下是一个稳健、无副作用的解决方案:
if type(__builtins__).__name__ == '_DefaultDict':
# ✅ 第二次执行:此时 __builtins__ 已替换,a 将被 __getattr__ 动态解析
def __getattr__(name):
return f'custom {name}'
print(a) # 输出: custom a
else:
# ⚠️ 首次执行:动态构造带 __missing__ 的 __builtins__ 替换体
import sys
import builtins
class _DefaultDict(dict):
def __missing__(self, name):
# 委托给模块级 __getattr__(注意:此时 __getattr__ 尚未定义,需确保在 exec 前定义)
try:
return __getattr__(name)
except NameError:
raise NameError(f"name '{name}' is not defined") from None
# 构建新 __builtins__:包含所有原始内置项 + __missing__ 行为
__builtins__ = _DefaultDict(vars(builtins))
del _DefaultDict # 清理临时类,避免污染命名空间
# 重新执行当前代码对象(即本模块顶层代码),使用新 __builtins__
exec(sys._getframe(0).f_code, globals())
✅ 关键设计说明:
- 使用
type(__builtins__).__name__判断是否已进入“重执行阶段”,避免无限递归; -
_DefaultDict.__missing__在__builtins__查找失败时触发,并显式调用模块定义的__getattr__; -
exec(..., globals())保证重执行时仍共享同一全局命名空间,使后续print(a)能正确命中__getattr__; - 兼容脚本直接运行(
python module.py)和作为模块被导入两种场景。
⚠️ 注意事项:
- 此方案依赖
sys._getframe(0),虽在 CPython 中稳定,但属于非公开 API,不推荐用于生产环境的高可靠性系统; - 若模块含副作用代码(如多次
open()、print()或状态变更),需确保重执行逻辑幂等或封装为惰性初始化; - 更安全的替代思路是主动封装访问入口(如提供
get(name)函数),而非强行模拟“全局变量动态解析”——后者违背 Python 显式优于隐式的哲学。
综上,该技巧展示了 Python 运行时元编程的灵活性,适用于原型开发、DSL 模块或教学演示,但实际工程中建议优先采用显式接口设计。










