☰
python中eval函数使用与异常处理详解
2026/10/15 6:55:10 网站建设 项目流程

前言


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或NoneNone全局命名空间

locals位置或关键字映射或NoneNone局部命名空间



返回的是表达式求值的结果,类型取决于表达式本身。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词条下的警告就是这个意思。真正需要"把字符串变成数据"时,按用途挑替代方案:




需求推荐做法说明



解析 JSONjson.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时也加上长度上限和异常处理。


总结




维度evalexecast.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 日起不再维护。)




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询