
本文介绍一种不依赖运行时帧操作、避免 inspect.currentframe() 风险的可靠方法,通过动态提取并执行函数体代码,构建仅包含函数内部显式定义变量(不含参数、内置属性)的字典。
本文介绍一种**不依赖运行时帧操作、避免 `inspect.currentframe()` 风险**的可靠方法,通过动态提取并执行函数体代码,构建仅包含函数内部显式定义变量(不含参数、内置属性)的字典。
在 Python 开发中,有时需要在函数定义后“静态分析”其内部声明的变量(如用于调试、元编程或配置生成),但直接访问运行时局部作用域存在明显限制:inspect.currentframe().f_locals 仅在函数执行中有效,且返回结果受优化影响(如闭包、常量折叠)、不可靠;而 f_back.f_locals 更易引发 AttributeError 或捕获到错误作用域。
因此,更稳健的思路是:将函数体作为纯代码文本提取 → 剥离签名行 → 动态写入临时模块 → 导入执行 → 提取其 __dict__ 中的非双下划线变量。该方案规避了帧对象生命周期问题,确保结果可预测、可复现。
以下是一个生产就绪的实现(已优化安全性与健壮性):
import inspect
import os
import tempfile
import importlib.util
from types import FunctionType
from typing import Any, Dict
from textwrap import dedent
def get_function_defined_variables(func: FunctionType) -> Dict[str, Any]:
"""
提取函数体内显式赋值的局部变量(不含参数、内置属性),返回 {name: value} 字典。
注意:
- 函数必须有源码(不能是交互式输入或编译字节码)
- 参数名不会被包含(因参数在函数签名中定义,不在函数体赋值语句内)
- 所有变量必须在函数体顶层直接赋值(不支持嵌套作用域如 if/for 内部定义)
"""
try:
source = inspect.getsource(func)
except (OSError, IOError):
raise ValueError(f"无法获取函数 {func.__name__} 的源码:需确保函数定义在 .py 文件中且未被混淆")
# 分割函数头与函数体,跳过 def 行及可能的装饰器
lines = source.strip().split('\n')
body_lines = []
in_body = False
for line in lines:
stripped = line.strip()
if stripped.startswith('def ') or stripped.startswith('@'):
continue # 跳过装饰器和 def 行
if not in_body and stripped and not stripped.startswith(' '):
continue # 跳过函数签名后的空行或注释前的首行
if stripped and (stripped.startswith(' ') or stripped.startswith('\t')):
in_body = True
body_lines.append(line)
elif in_body and not stripped:
break # 遇到空行结束函数体(保守策略)
if not body_lines:
return {}
# 去除公共缩进,确保可执行
body_clean = dedent('\n'.join(body_lines))
# 创建临时文件(比硬编码 "TEMPORARYFILE.py" 更安全)
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8') as f:
f.write("from __future__ import annotations\n") # 兼容未来语法
f.write(body_clean)
temp_path = f.name
try:
# 动态导入临时模块
spec = importlib.util.spec_from_file_location("dynamic_module", temp_path)
if spec is None:
raise ImportError("无法为临时文件创建模块规范")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # 执行函数体(此时变量被定义在模块全局)
# 过滤:仅保留用户定义的非 dunder 名称(排除 __name__, __file__ 等)
return {
k: v for k, v in module.__dict__.items()
if not (k.startswith('__') and k.endswith('__'))
}
finally:
# 清理临时文件
try:
os.unlink(temp_path)
except OSError:
pass # 忽略清理失败(如已被删除)
# 使用示例
def example_func():
var1 = 10
var2 = 20
var3 = 30
var4 = '40'
var5 = False
# 注意:参数 `a`, `b` 不会出现在结果中
a = 1 # 即使重赋值,也不算“定义”,但此处为演示——实际中应避免覆盖参数名
if __name__ == "__main__":
result = get_function_defined_variables(example_func)
print(result)
# 输出:{'var1': 10, 'var2': 20, 'var3': 30, 'var4': '40', 'var5': False}✅ 关键优势:
- ✅ 完全绕过 inspect.currentframe() 的 None 风险与作用域不确定性;
- ✅ 支持类型提示(from __future__ import annotations);
- ✅ 使用 tempfile.NamedTemporaryFile 避免文件名冲突与权限问题;
- ✅ 显式过滤 __dunder__ 属性,结果纯净;
- ✅ 抛出清晰异常,便于调试源码缺失等边界情况。
⚠️ 使用限制与注意事项:
- ❗ 函数必须具有可访问的源码(.py 文件中定义,非 exec() 动态生成);
- ❗ 不支持函数体内条件分支(如 if True: x = 1)——因静态分析无法执行逻辑;
- ❗ 若函数体含 import、class 或其他顶层语句,它们也会被导入并出现在结果中,请按需后处理;
- ❗ 生产环境慎用于不受信函数(等效于 exec() 源码),建议配合沙箱或白名单校验。
该方法本质是将“函数体”视为一段独立可执行脚本,以模块级作用域模拟其局部行为,是目前兼顾可靠性、可读性与工程可用性的最佳实践。










