Python 是一门极其灵活的语言,而 exec 和 eval 则是这种灵活性最极端的体现------它们能让你在运行时把一段字符串当作代码来执行。这种能力听起来很酷,用起来却像在玩火。本文从原理出发,深入剖析这两个函数的本质差异、潜藏的安全地雷,以及在真实工程中如何做出理智的取舍。
一、它们到底是什么?
eval 和 exec 都是 Python 的内置函数,但定位截然不同。
eval 专门用于求值表达式,它接收一个字符串,把它当作一个 Python 表达式来计算,并返回结果:
python
result = eval("1 + 2 * 3")
print(result) # 输出 7
exec 则更"野",它可以执行任意的 Python 代码块 ,包括函数定义、循环、赋值等,但不返回值(返回 None):
python
code = """
def greet(name):
return f"Hello, {name}!"
"""
exec(code)
print(greet("World")) # 输出 Hello, World!
两者的核心区别可以这样理解:eval 是一个计算器 ,只能算出一个值;exec 是一个解释器,能跑完整的程序。
二、底层原理:Python 其实在偷偷编译
很多人以为 exec / eval 是"直接解释字符串",其实不然。CPython 在执行这两个函数时,会先把字符串编译成字节码(bytecode) ,再交给虚拟机执行------这和正常 import 一个模块的流程本质上是一样的。

这意味着两件事:
- 性能损耗是真实存在的 :每次调用都要经历编译,比直接调用函数慢得多。如果在循环里反复
eval,性能会很难看。 compile()可以预编译 :如果你确实需要反复执行同一段动态代码,可以先用compile()生成 code object,再传给exec,避免重复编译的开销。
三、安全噩梦:为什么说它们是"装了子弹的枪"
这是最关键的部分。eval 和 exec 的危险程度,远超大多数开发者的想象。
3.1 任意代码执行(RCE)
假设你写了一个"在线计算器",用 eval 来处理用户输入:
python
# 危险!永远不要这样做
user_input = input("请输入表达式:")
result = eval(user_input)
用户只需要输入:
python
__import__('os').system('rm -rf /')
你的服务器就可能被清空。这就是远程代码执行(RCE) 漏洞------攻击者可以通过精心构造的字符串,在你的服务器上执行任意系统命令。
3.2 "沙箱"是假的
很多人会想到"限制命名空间"来防御:
python
# 看起来很安全?其实不是
eval(user_input, {"__builtins__": {}})
但这种防御形同虚设。Python 对象系统极其复杂,攻击者可以通过对象的 __class__、__subclasses__、__globals__ 等魔术属性,绕过任何手工构造的沙箱,最终找到通往系统调用的路径。
一个经典的绕过姿势:
python
# 即使 builtins 被清空,也能找到 os 模块
eval("().__class__.__bases__[0].__subclasses__()")
这会列出所有子类,其中往往藏着可以访问文件系统的类。
3.3 风险全景图

四、更安全的替代方案
在大多数场景下,你根本不需要 eval 或 exec,换个思路就能解决问题。
4.1 ast.literal_eval:只解析数据,不执行代码
如果你只是想把一个字符串形式的 Python 字面量(数字、列表、字典等)转换成对象,用 ast.literal_eval 就够了,它只允许解析安全的字面量,拒绝任何可执行代码:
python
import ast
# 安全:只解析字面量
data = ast.literal_eval("{'name': 'Alice', 'age': 30}")
print(data) # {'name': 'Alice', 'age': 30}
# 会抛出异常,而不是执行
ast.literal_eval("__import__('os').system('ls')") # ValueError!
4.2 用字典替代动态变量名
Reddit 上那个游戏开发的例子很典型------用 eval(weaponName + ".get_stat()") 来动态访问对象。正确的做法是用字典:
python
# 不好的写法
thing = eval(nameOfWeapon + ".get_itemStat()")
# 好的写法:用字典存储对象
weapons = {
"sword": Sword(),
"bow": Bow(),
}
thing = weapons[nameOfWeapon].get_itemStat()
4.3 getattr:动态访问属性
需要动态访问对象属性时,getattr 是正确选择:
python
# 不好的写法
eval(f"obj.{method_name}()")
# 好的写法
getattr(obj, method_name)()
4.4 替代方案对比
| 场景 | 危险做法 | 安全替代 |
|---|---|---|
| 解析数据字符串 | eval("{'a': 1}") |
ast.literal_eval(...) |
| 动态访问属性 | eval(f"obj.{attr}") |
getattr(obj, attr) |
| 动态调用方法 | eval(f"obj.{method}()") |
getattr(obj, method)() |
| 按名称获取对象 | eval(name) |
dict[name] 或 getattr(module, name) |
| 解析 JSON | eval(json_str) |
json.loads(json_str) |
CITE_2
五、工程实践:什么时候可以用,怎么用
exec 和 eval 并非一无是处,在某些特定场景下它们是合理的工具。关键在于:你是否完全控制了输入来源。
5.1 合理的使用场景
- REPL / 交互式解释器 :Jupyter Notebook、IPython 本质上就是在用
exec执行用户输入的代码,但用户就是自己,风险可控。 - 代码生成工具:元编程、ORM 框架在内部动态生成类或方法时,输入完全由框架自身控制。
- 配置文件执行 :某些框架(如 web2py)用
exec加载配置,但这些配置文件来自可信的本地文件系统。 - 测试框架 :
pytest等工具在内部用exec动态执行测试代码。
5.2 工程中的决策树

5.3 如果非用不可,这样做
python
import traceback
def safe_exec(code: str, allowed_globals: dict = None):
"""
一个相对安全的 exec 封装(仅适用于可信输入)
"""
# 1. 限制全局命名空间,只暴露必要的内容
safe_globals = {
"__builtins__": {
"print": print,
"range": range,
"len": len,
# 只暴露你真正需要的内置函数
}
}
if allowed_globals:
safe_globals.update(allowed_globals)
# 2. 捕获所有异常,避免崩溃
try:
exec(compile(code, "<string>", "exec"), safe_globals)
except Exception as e:
print(f"执行失败:{traceback.format_exc()}")
即便如此,这种"安全封装"也只适用于可信输入,对于真正的外部用户输入,没有任何手工沙箱是可靠的。CITE_4
六、总结
eval 和 exec 是 Python 动态性的极致体现,但它们更像是语言留给框架开发者和工具作者的"后门",而非普通业务代码的常规工具。
一句话概括工程原则:如果输入来自用户,永远不要碰 eval/exec;如果输入来自自己,先想想有没有更简单的替代方案。 大多数时候,ast.literal_eval、getattr、字典映射就已经够用了。真正需要动态执行代码的场景,往往意味着你在构建一个解释器或框架------那时候,你自然会知道自己在做什么。CITE_2CITE_4
参考来源