2.34. 动态代码

有三个内置函数接受一段 Python 源码字符串并运行它:eval()exec()compile()。它们合在一起让代码能够在运行时构造并执行更多代码——这偶尔正是恰当的工具,但更多时候是 bug 和安全漏洞的来源。

警告

这些函数会执行任意的 Python 代码。把用户输入传给 evalexec —— 比如用户可以编辑的文件中的字符串、通过网络收到的负载、在 REPL 提示符下键入的值 —— 会让这些输入做调用脚本所能做的任何事情,乃至删除设备上的每一个文件。请谨慎使用它们,绝不要在自动运行的代码中使用,也绝不要对你无法控制的数据使用。

2.34.1. eval

eval() 运行单个表达式并返回其值:

>>> eval("3 * 7")
21
>>> name = "OpenMV"
>>> eval("name.lower()")
'openmv'

表达式默认能看到调用者的全局和局部变量,这就是为什么第二个例子中 name 能被解析。传入显式的字典可以让你对求值进行沙箱隔离:

eval("a + b", {"__builtins__": None}, {"a": 1, "b": 2})

即便经过沙箱隔离,eval 依然危险。有一些众所周知的技术可以逃逸出这类沙箱;对于不可信的输入,不要仅依赖空 __builtins__ 这种小把戏。

2.34.2. exec

exec() 运行的是一代码而不是单个表达式——包括语句、函数定义、循环——并返回 None

exec("for i in range(3): print(i)")

输出:

0
1
2

这段代码可以定义在之后可用的名称,但关于局部作用域与全局作用域有一些注意事项。在函数内部使用 exec 很少会按编写者预期的方式工作;如果你确实需要它,请在模块层级运行它。

2.34.3. compile

compile() 把一段源码字符串转换成一个代码对象,之后可以传给 eval()exec()。当同一段源码将多次运行时使用它——解析只发生一次,执行更快:

expr = compile("x * x", "<expr>", "eval")
for x in range(5):
    print(eval(expr))

输出:

0
1
4
9
16

中间的参数是一个标签,如果代码抛出异常,它会出现在回溯信息中。第三个参数对单个表达式是 "eval",对一段代码是 "exec",对会打印其结果的交互式语句是 "single"

2.34.4. 何时该动用它们

几乎永远不该。初学者设想的大多数用例都有更安全的替代方案:

  • 读取配置文件。 使用 json —— 结构化数据,不执行代码。

  • 对用户键入的数值进行求值。 使用 int() / float() 来解析,然后做算术运算。如果用户确实需要输入一个公式,请使用一个小型的表达式解析器,而不是 eval

当你确实需要 eval / exec / compile 时,请隔离调用点,记录即将执行的确切字符串,并把这段源码当作代码中最可疑的东西来对待。