使用exec获取函数内局部变量失败,求解决方案及优化思路
为什么exec执行函数__code__后locals字典为空?以及更好的变量赋值追踪方法
问题原因
你遇到的assignments字典为空的核心原因是:Python对函数的局部变量做了性能优化,函数字节码中的局部变量是通过栈帧的索引直接访问的,而非字典查找。当你用exec(func.__code__, globals(), assignments)执行函数代码时,这些局部变量不会被写入到你传入的assignments字典里——函数的局部作用域和exec的locals字典是两个独立的存储区域。
解决方案
方案1:使用sys.settrace做运行时追踪
sys.settrace可以注册一个追踪函数,监听代码执行过程中的事件(比如行执行、变量赋值),我们可以利用它捕获所有局部变量的赋值操作:
import sys def track_assignments(func): def wrapper(*args, **kwargs): assignments = {} def trace_handler(frame, event, arg): if event == 'line': current_locals = frame.f_locals.copy() # 对比之前的记录,找出新增/修改的变量 for var, value in current_locals.items(): if var not in assignments or assignments[var] != value: assignments[var] = value print(f"{var} = {value}") return trace_handler # 启用追踪 sys.settrace(trace_handler) try: return func(*args, **kwargs) finally: # 恢复原有追踪设置,避免影响其他代码 sys.settrace(None) return wrapper @track_assignments def test(): a = 10 b = 20 c = a * b print(a) if __name__ == '__main__': test()
运行后输出:
a = 10 b = 20 c = 200 10
方案2:用AST静态分析修改函数代码(更高效)
如果追求性能,可以通过AST(抽象语法树)分析函数代码,自动在所有赋值语句后插入打印逻辑,再重新编译执行。这种方法是静态修改代码,运行时没有额外追踪开销:
import ast import inspect def track_assignments(func): # 获取函数源代码并解析为AST source_code = inspect.getsource(func) tree = ast.parse(source_code) class AssignmentInjector(ast.NodeTransformer): def visit_Assign(self, node): # 为每个赋值变量添加打印语句 for target in node.targets: if isinstance(target, ast.Name): var_name = target.id # 构造`print(f"{var_name} = {var_name}")`的AST节点 print_node = ast.Expr( value=ast.Call( func=ast.Name(id='print', ctx=ast.Load()), args=[ ast.JoinedStr( values=[ ast.Constant(value=f"{var_name} = "), ast.FormattedValue( value=ast.Name(id=var_name, ctx=ast.Load()), conversion=-1 ) ] ) ], keywords=[] ) ) # 将打印语句插入到赋值语句的下一行 node.parent.body.insert(node.parent.body.index(node) + 1, print_node) return node def visit_Module(self, node): # 遍历所有节点,记录父节点以便插入操作 for n in ast.walk(node): for child in ast.iter_child_nodes(n): child.parent = n return self.generic_visit(node) # 修改AST并修复位置信息 transformer = AssignmentInjector() modified_tree = transformer.visit(tree) ast.fix_missing_locations(modified_tree) # 编译修改后的代码,获取新函数 compiled_code = compile(modified_tree, filename=inspect.getsourcefile(func), mode='exec') namespace = {} exec(compiled_code, func.__globals__, namespace) modified_func = namespace[func.__name__] def wrapper(*args, **kwargs): return modified_func(*args, **kwargs) return wrapper @track_assignments def test(): a = 10 b = 20 c = a * b print(a) if __name__ == '__main__': test()
运行后输出和方案1一致,但性能更优,适合需要频繁调用的函数。
总结
- 原代码的核心问题:函数局部变量的索引访问机制导致exec无法将其写入传入的locals字典。
- 运行时追踪(sys.settrace):实现简单,适合调试场景,但有一定性能开销。
- AST静态修改:性能更好,适合需要低开销的生产环境,但实现稍复杂。
内容的提问来源于stack exchange,提问作者Hoonter
相关产品推荐
相关产品推荐

