前言
eval()是内置函数里争议最大的一个:它能把字符串当成 Python 表达式求值,用起来非常方便——一个计算器、一段可配置的公式,几行就写完;但它同时是能执行任意代码的入口,官方文档在它的词条下直接挂了警告:用它处理来自用户的不受信任输入会导致安全漏洞。
围绕eval()有两种典型误解。第一种是"我给globals传个空字典就安全了"——官方文档明确说明这不是安全机制,被执行的代码仍然能访问所有内置函数。第二种是把eval()和exec()混为一谈:eval只能求表达式并返回它的值,exec执行的是语句且返回值恒为None。
本文讲清eval的签名、命名空间行为、会抛出哪些异常与怎么写异常处理,并给出安全的替代方案。示例基于 Python 3.8+,其中用到较新特性的地方会单独注明。Python 2 的eval与 Python 3 方向一致,但 Python 2 的输入处理函数raw_input在 Python 3 中已改名为input,且 Python 2.7 已停止维护,本文一律用 Python 3 写法。
一、真实签名与返回值
官方签名是:
eval(source, /, globals=None, locals=None)| 参数 | 位置/关键字 | 类型 | 默认值 | 含义 |
|---|
source | 仅位置(/之前) | str或 code 对象 | 无 | 要求值的表达式 |
globals | 位置或关键字 | dict或None | None | 全局命名空间 |
locals | 位置或关键字 | 映射或None | None | 局部命名空间 |
返回的是表达式求值的结果,类型取决于表达式本身。globals与locals自 Python 3.13 起才能按关键字传递;3.12 及更早版本里它们是仅位置参数,写eval("1+1", globals={})在该版本会抛TypeError。
# 适用于 Python 3.8+
print(eval("1 + 2")) # 3,int
print(eval("1, 2")) # (1, 2),表达式列表返回元组
print(eval("'a' * 3")) # aaa
print(eval("[i * i for i in range(3)]")) # [0, 1, 4]
print(eval("max(1, 5, 3)")) # 5注意eval收到字符串时会去掉首尾的空格与制表符再解析。另外如果传入的是用'exec'模式编译出来的 code 对象,eval的返回值就是None。
二、命名空间:globals 与 locals
不传命名空间时,eval用调用处的全局与局部环境。传入后规则如下:
- 若
globals里没有__builtins__这个键,解释器会在解析前自动塞入一份内置模块的引用; - 若省略
locals,它就默认等于globals字典; - 只传
globals不传locals时,表达式的所有名字都从这一份字典里找。
# 适用于 Python 3.8+
x = 10
print(eval("x + 1")) # 11,用调用处的环境
print(eval("x + y", {"x": 1, "y": 2})) # 3,完全用传入的字典
print(eval("y", {"y": 5})) # 5
# 内置函数默认仍然可见
print(eval("len([1, 2, 3])", {"__builtins__": {}}))
# 抛 NameError: name 'len' is not defined,因为显式屏蔽了内置上面最后一行恰好演示了文档的警告:把__builtins__设成空字典可以挡住名字查找,但它不是安全边界——攻击者仍可用().__class__之类的属性链绕过,所以不要把这种写法当沙箱。
三、eval 会抛出哪些异常
eval本身只在解析阶段抛SyntaxError;表达式求值过程中的异常,原样从被求值的代码里冒出来。
| 异常 | 触发场景 | 示例 |
|---|
SyntaxError | 表达式语法不合法 | eval("1 +") |
NameError | 名字未定义 | eval("undefined_name") |
TypeError | 类型不匹配的操作 | eval("1 + 'a'") |
ZeroDivisionError | 除零 | eval("1 / 0") |
ValueError | 表达式内部转换失败 | eval("int('abc')") |
MemoryError/RecursionError | 极端构造 | 深递归表达式 |
写异常处理时要具体捕获,不要裸except::
# 适用于 Python 3.8+
def safe_calc(expr, allowed=None):
"""只允许访问指定的名字,失败返回 None 并说明原因。"""
env = {"__builtins__": {}}
if allowed:
env.update(allowed)
try:
return eval(expr, env)
except SyntaxError as e:
print("表达式语法错误:", e)
except NameError as e:
print("用了未定义的名字:", e)
except (TypeError, ZeroDivisionError, ValueError) as e:
print("求值失败:", type(e).__name__, e)
return None
print(safe_calc("x + 1", {"x": 41})) # 42
print(safe_calc("1 +")) # 语法错误
print(safe_calc("y")) # 名字未定义
print(safe_calc("1 / 0")) # 求值失败 ZeroDivisionError裸except:会连KeyboardInterrupt(用户按 Ctrl+C)和SystemExit一起吞掉,程序变得无法中断,这是很典型的事故来源。
四、安全边界:别让 eval 碰不可信输入
结论先给:任何来自用户、网络、文件、第三方接口的字符串,都不应该交给eval或exec。官方文档在eval词条下的警告就是这个意思。真正需要"把字符串变成数据"时,按用途挑替代方案:
| 需求 | 推荐做法 | 说明 |
|---|
| 解析 JSON | json.loads(s) | 只产生基础类型,不执行代码 |
| 解析字面量(元组/列表/字典/数值) | ast.literal_eval(s) | 不执行代码,但见下方注意 |
| 字符串转数字 | int(s)/float(s)/Decimal(s) | 精确、可预期 |
| 表达式计算器 | 手写ast遍历 + 白名单运算符 | 只放开需要的节点类型 |
| 解析配置 | configparser/tomllib(3.11+) | 明确的数据格式 |
关于ast.literal_eval要特别说明:它不执行 Python 代码,没有名字查找、不能调用函数,比eval安全得多。但官方文档同时也指出,它对恶意输入并非免费午餐——很小的输入也可能造成内存耗尽、C 栈耗尽或过量 CPU 占用,因此文档现在的措辞是"不建议对不受信任的数据调用它"。所以正确的心态是:它适合处理"你自己生成、只是需要解析"的字符串;面对外部输入,优先用json,并对长度和复杂度设上限。
需要更严的表达式求值时,可以解析成 AST 后自己走白名单:
# 适用于 Python 3.8+
import ast
import operator
OPS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
}
def calc(node):
if isinstance(node, ast.Expression):
return calc(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp) and type(node.op) in OPS:
return OPS[type(node.op)](calc(node.left), calc(node.right))
raise ValueError("不支持的表达式")
print(calc(ast.parse("(1 + 2) * 3", mode="eval"))) # 9
# calc(ast.parse("__import__('os').system('echo hi')", mode="eval")) 会抛 ValueError这种写法的好处是"能做什么"由你的白名单显式决定,而不是靠"禁用某些名字"去堵。
常见坑点
坑 1:把 eval 用在用户输入上。
❌eval(input("请输入表达式: "))—— 输入__import__("os").system("...")之类的字符串就会执行系统命令。
✅ 用int()/float()转换,或改用 AST 白名单求值;确实要算数,可参考ast方案。
坑 2:以为传空globals就是沙箱。
❌ 认为eval(s, {"__builtins__": {}})就绝对安全。
✅ 读过文档就知道那是名字可见性控制,不是安全机制。不要用 eval 处理不可信内容,这一步不能省。
坑 3:把 eval 和 exec 搞混。
❌eval("x = 1")—— 赋值是语句不是表达式,抛SyntaxError。
✅ 要用exec("x = 1");反过来exec("1 + 1")不会返回2,它的返回值恒为None(exec自 3.11 起还多了仅关键字的closure参数)。
坑 4:globals传了字典却忘了名字从哪找。
❌eval("x + 1", {})—— 抛NameError: name 'x' is not defined,空字典里没有x。
✅ 把需要的名字显式放进字典:eval("x + 1", {"x": 1})。
坑 5:用裸except:包住 eval。
❌
try:
v = eval(s)
except:
v = None✅ 明确写except (SyntaxError, NameError, TypeError, ZeroDivisionError, ValueError):,别把KeyboardInterrupt一起吞掉。
坑 6:以为 3.13 之前也能按关键字传 globals。
❌ 在 3.12 上写eval("1+1", globals={})—— 抛TypeError,因为那时globals是仅位置参数。
✅ 一律用位置参数eval(s, env)最兼容;升级到 3.13+ 才谈得上关键字传法。
坑 7:把 eval 的返回值当 str。
❌eval("'1'") + 1—— 返回的是str类型的'1',字符串加整数抛TypeError。
✅ 明确表达式会返回什么类型;不做假设,拿到后先isinstance判断或用类型转换函数。
坑 8:把ast.literal_eval当成万能的安全解析器。
❌ 对超长、深层嵌套的外部字符串直接调用它 —— 文档明确提到可能造成内存/栈/CPU 耗尽。
✅ 面对外部输入优先json.loads,并限制输入长度;确需literal_eval时也加上长度上限和异常处理。
总结
| 维度 | eval | exec | ast.literal_eval |
|---|
| 处理对象 | 表达式 | 语句(含赋值、导入等) | 仅字面量容器 |
| 返回值 | 表达式的值 | 恒为None | 解析出的字面量对象 |
| 能执行代码 | 能 | 能 | 不能 |
| 可用于不可信输入 | 绝对不行 | 绝对不行 | 文档不建议,有资源耗尽风险 |
| 典型用途 | 受控的表达式求值 | 动态执行语句 | 解析自己生成的字符串 |
eval的正确用法是"我完全掌控输入内容"的场景,例如内部 DSL、受控的公式配置;一旦输入跨越信任边界,就必须换成json、int/float/Decimal、或基于ast的白名单求值。异常处理上,永远具体捕获、不要裸except:,并让函数在失败时返回明确的信号而不是抛出难以定位的异常。
(Python 2 中eval/exec同为内置,但exec在 Python 2 里是语句(exec "code"),Python 3 中才统一为函数exec("code");Python 2 的raw_input()也已被 Python 3 的input()取代。Python 2.7 自 2020 年 1 月 1 日起不再维护。)