exec/eval 报错难定位因 lineno 相对字符串起始,需用 compile() 捕获完整上下文;eval 仅支持单表达式,赋值等语句须用 exec;作用域隔离易致 NameError;用户输入需白名单+禁用 builtins 防注入。

exec/eval 报错时看不到具体哪行出问题
默认情况下,exec 或 eval 抛出的 SyntaxError 的 lineno 和 offset 是相对于传入字符串的起始位置,但错误信息里不显示原始代码上下文,容易误判。比如你拼接了一段含换行的字符串,报错说第 3 行错了,但你根本不知道这“第 3 行”对应你源码里的哪一段。
解决办法是手动构造 ast.PyCF_ONLY_AST 阶段的解析,或更实用的是:用 compile() 先编译,捕获异常再补全上下文:
code_str = "x = 1\ny =\nz = 3"
try:
compile(code_str, "<dynamic>", "exec")
except SyntaxError as e:
print(f"Syntax error at line {e.lineno}, offset {e.offset}: {e.text}")
# 输出:Syntax error at line 2, offset 4: y =\n
</dynamic>
-
compile()的第三个参数必须和你要执行的方式一致:"exec"(多语句)、"eval"(单表达式)、"single"(交互式) -
filename参数(第二个)建议设为有含义的值(如"),否则 traceback 里显示" 不利于定位 - 注意:直接
exec(code_str)报错时,SyntaxError的text字段可能为None(尤其在某些 Python 版本或嵌入场景下),而compile()总能保证有值
eval 不能执行赋值或 import 语句
这是常见误用:把一段含 x = 5 或 import json 的字符串丢给 eval(),结果报 SyntaxError: invalid syntax。因为 eval() 只接受**单个表达式**,不支持语句。
区分清楚使用场景:
- 想计算表达式(如
"2 + 3 * 4"、"len(my_list)")→ 用eval() - 想执行语句(定义变量、调用函数、循环、导入模块)→ 必须用
exec() - 混合场景(先
exec初始化环境,再eval计算结果)要显式传入globals和locals字典,否则作用域隔离导致找不到变量
示例:
ns = {}
exec("import math; x = math.pi ** 2", ns)
result = eval("round(x, 2)", ns) # ✅ 正确:共享命名空间
# eval("x") # ❌ 若没传 ns,会 NameError
动态执行时的 NameError 实际是语法/作用域问题
看起来像变量没定义(NameError: name 'xxx' is not defined),但根源常不在运行时——而是 exec 执行失败后没真正定义变量,或者作用域没透传。典型诱因:
- 用了
exec(code, {})却没保存返回的字典,新变量只活在空字典里,外面完全不可见 - 在函数内调用
exec且未声明global或nonlocal,无法修改外层变量(Python 3.12+ 对此限制更严格) -
exec字符串本身有隐藏字符(如 BOM、不可见零宽空格),导致解析中断,后续变量未定义
调试建议:
- 执行前先
print(repr(code_str))看是否有异常空白符 - 强制捕获并打印
exec后的命名空间:exec(code_str, ns); print(list(ns.keys())) - 避免在函数体里直接
exec修改局部变量;如需,改用exec(code_str, globals(), locals())并注意locals()在函数内是只读快照(CPython 行为),不可靠
安全执行用户输入时,syntax error 可能是攻击试探
如果你开放了类似“公式计算器”或“规则引擎”的接口,用户传入的字符串报 SyntaxError,别急着当普通错误处理——这可能是试探性注入,比如尝试插入 __import__、分号、换行加注释绕过校验等。
防御要点:
- 不要仅依赖语法检查来防注入;
SyntaxError被绕过成本很低(例如用getattr替代点号、用chr()拼字符串) - 对输入做白名单预处理:只允许数字、运算符、预设函数名、括号;用正则粗筛(如
re.match(r'^[0-9+\-*/().\s\w]+$', user_input)),再进compile() - 生产环境禁用
__builtins__:传入干净的globals={'__builtins__': {}},再按需添加如math、abs等受限函数
复杂点在于:语法检查只是第一道过滤,真正的风险往往藏在合法语法包裹的动态行为里——比如 eval("getattr(__import__('os'), 'system')('id')") 完全合法,但已越权。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











