Python 的 exec 与 eval :动态代码执行的能力、风险与工程实践

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


参考来源

相关推荐
卷无止境3 分钟前
WebGIS生态全景丨从浏览器里的地图到背后的空间数据库
后端·python
Y3815326629 分钟前
竞品上新监控:用搜索 API 盯住对手发布了什么新功能
python·搜索引擎
benchmark_cc9 分钟前
如何设计一个 Python 实时行情获取程序?从轮询、数据处理到量化策略接入
python·数据分析·pandas·量化交易·股票数据·quantdash
JuiceFS18 分钟前
JuiceFS 企业版 5.4:从千亿文件到百万客户端
后端
Metaphor69219 分钟前
Python 实现 Word/TXT 互转:附完整代码
python·word·格式转换·txt
Lost of 程序猿20 分钟前
命令模式实战:把“下单后的动作“打包成可执行的任务
后端·设计模式·c#·asp.net·命令模式
焦玉全23 分钟前
Sharding-JDBC 分库分表实战:从 800 万订单表的查询优化说起
后端
王中阳Go25 分钟前
面试官问"你怎么证明它有效",200个转AI的后端没几个答得上来
人工智能·后端·面试
深入云栈39 分钟前
Netty 4.2.x 源码深度解析 (十四):KQueue 传输 —— macOS 高性能 IO 的 Netty 实现
java·后端