# Python中的exec一个被误解的实用工具从一段经历说起几年前我在维护一个遗留系统时遇到一个棘手的问题。系统中有一个配置模块需要根据不同的业务场景动态加载不同的计算逻辑。最初的实现方式是写死了几百个if-else分支每次新增业务逻辑都要修改核心代码既脆弱又难以维护。当时团队里有人提出用exec来解决会议室里立刻炸开了锅。有人说exec很危险不要用有人说不规范会被代码审查拒绝还有人拿出了Python官方文档里那段关于eval和exec的警告。但最终我们就是用exec解决了问题。几年过去了这套系统依然运行稳定开发新业务逻辑时只需要提供一个Python格式的配置文件即可。这让我们重新思考exec这个工具。它确实有风险但就像菜刀一样关键是弄清楚什么时候用、怎么用。exec到底是什么exec是一个内置函数执行一段动态生成的Python代码。它接收一个字符串把这个字符串当作Python代码来执行。如果有人把这理解为简单的字符串执行那就太片面了。exec和eval、compile这些函数一起构成了Python动态执行的核心机制。它们允许程序在运行时理解和运行自己生成的代码。这种能力在静态语言里几乎不可能做到但在Python中是完全合法的。这得益于Python的设计哲学之一——运行时有完整的编译器和解释器环境。它能做什么exec最典型的应用场景是动态构建执行逻辑。想象这样一个场景用户可以通过Web界面配置数据的处理规则这些规则是动态变化的。如果不使用exec可能需要设计复杂的配置结构然后写大量if-else来解析。但用exec的话直接把用户编写的规则字符串拿去执行就好了。另一个常见场景是做测试和调试工具。比如开发一个沙盒环境让用户提交Python代码片段看运行结果。Python的交互式解释器背后就有类似的机制。我还见过在科学计算中使用exec的例子。研究者需要自动化实验流程每次实验的参数和计算逻辑都不相同。通过把计算逻辑编写为字符串并保存下来后续可以通过exec重新运行相同流程方便复现。另外就是代码生成。有些框架会动态生成代码来提升性能。比如ORM框架中的某些查询构建根据用户输入生成特定的SQL查询代码然后exec执行。怎么用基础用法很简单。exec接收一个字符串然后执行它。codeprint(hello world)exec(code)exec还可以接收一个命名空间参数。当要执行的代码需要访问外部变量或者执行后想要获取产生的变量时就需要指定命名空间。namespace{a:1,b:2}exec( def add(): return a b result add() ,namespace)print(namespace[result])# 输出3这里的关键是通过传入和传出的字典控制代码的变量作用域。这样做的好处是避免了污染当前作用域而且能清晰地看到哪些变量被修改了。exec和compile配合使用可以做一些更有趣的事情。compile把字符串编译成字节码对象exec执行这个对象。如果需要反复执行同一段代码预编译能提升性能。compiledcompile(print(x y),string,exec)foriinrange(100):exec(compiled,{x:i,y:i1})最佳实践首先是要明确一个界限exec不应该出现在对外的API接口中。如果一个Web服务的端点接受用户输入并通过exec执行那就等于把服务器直接交出去了。用户的输入需要经过严格的验证和过滤。使用exec时最好创建一个新的命名空间来隔离环境。这就像Python的模块系统一样通过边界来限制影响范围。新创建的字典作为命名空间传递给exec使得被执行的代码只能在有限的范围内操作。另一个容易被忽略的点是异常处理。动态执行的代码可能抛出的异常类型未知这些异常如果向上冒泡会把调用栈暴露给上层。合理的做法是在exec外部包裹try-except把异常转化成可控的错误信息。安全性方面有人提出用exec的globals参数传入受限的内置函数。比如把__builtins__设置成空字典或一个受限的字典阻止import、open等危险操作。但说实话对高安全意识的应用场景这不足以完全防止攻击。Python的动态特性太丰富总能找到绕过的方法。一个实用的替代思路是如果只是需要动态计算表达式优先考虑eval或ast.literal_eval。ast.literal_eval只接受字面量列表、字典、字符串等不会执行任意代码安全性好得多。与eval和compile的对比eval和exec最大的区别在于eval接受表达式并返回结果exec接受语句但不返回结果。resulteval(1 2)# result 3exec(x 1 2)# 不返回结果但x在命名空间中存在从安全角度来看eval的危险程度和exec差不多因为它们都能执行任意代码。但场景不同eval通常用在需要动态计算表达式的场合而exec用在需要完整执行代码块的场合。compile则更像是底层工具。它把字符串编译成字节码然后由exec或eval执行。当同一段代码需要反复执行时预编译能减少重复编译的开销。拿生活中的例子来类比eval就像是计算器输入表达式得出结果exec像是一个小的脚本解释器可以定义变量、运行循环compile则是把脚本预编译成可执行格式的过程。三者的选择依赖于具体的需求。如果只是计算用户输入的简单表达式eval就够了。如果需要运行完整的程序逻辑则需要exec。如果对性能要求高且代码是固定的则可以考虑compile加exec的组合。说到性能exec的开销主要在编译阶段。一旦代码完成编译后续的执行速度与预先写# # Python eval 函数一个老司机的视角他是什么eval 这玩意儿说白了就是把字符串当代码执行。想象一下你写了一行11正常情况下这就是个字符串但 eval 能把它变成真的加法运算返回2。Python 解释器就像个翻译官而 eval 就是给这个翻译官递小纸条的人。不过这里有个微妙的点。很多人觉得 eval 就是个“字符串转表达式”的工具但实际上它的能力边界比这个更宽泛——只要符合 Python 语法规则的表达式它都能处理。赋值语句不行函数定义不行但列表推导式、三元表达式、甚至 lambda 表达式都可以。有一次我在调试一个数据管道看到同事用 eval 动态生成了一串复杂的过滤条件我当时就在想这玩意儿是把双刃剑。他能做什么实际开发中eval 最常用的场景是处理用户输入的数学表达式。比如一个计算器程序用户输入3*4(5-2)“eval 直接就能算出结果。再比如在做配置管理的时候有些人会把配置项写成一个字符串表达式像config.max_connections * 2”用 eval 解析出来。比较有意思的一个用法是动态属性访问。假设你有一个嵌套很深的字典想通过一个字符串路径去取值像data[‘users’][0][‘name’]用 eval 就能直接拿到结果。当然这也可以用安全的方式实现但有些老代码里确实会这么写。还有一个稍微冷门但实用的场景测试 mock 数据。写测试用例的时候有时候需要快速生成一个随机表达式eval 配合字符串拼接挺方便的。不过这些场景下我都会加个全局和局部变量的限制后面会说到。怎么使用基本用法很简单就一行resulteval(1 2)print(result)# 3但真正让我觉得有用的是 eval 的三个参数。第一个是表达式字符串第二个是全局变量字典第三个是局部变量字典。举个例子x10resulteval(x 5,{x:100})print(result)# 105这里我把全局环境限制在一个字典里外面的 x 根本影响不到。这种写法在解析模板表达式时特别有用。比如你有一个 HTML 模板里面嵌了 Python 表达式就可以用这种方式控制上下文。另外要注意的是eval 不支持多行代码也不支持语句比如 for 循环、函数定义。如果你执行 eval(“for i in range(5): print(i)”)会直接报语法错误。所以想把 eval 当动态代码执行用的人最终都会转向 exec。最佳实践说到底eval 最大的问题是安全。你永远无法保证用户输入的字符串里不会出现 “import(‘os’).system(‘rm -rf /’)” 这种内容。所以我的经验是能用 ast.literal_eval 代替的就用 ast.literal_eval它只能解析字面量像字符串、数字、列表、字典这些安全得多。实在要用 eval一定把全局和局部变量限制成空字典或者只放必要的函数。比如safe_evallambdaexpr:eval(expr,{__builtins__:{}},{})这样连内置函数都访问不了只能执行纯数学运算。但即使这样还是要小心字符串拼接的问题。有一次我看到一个代码用 eval 处理用户输入的 JSON 路径结果因为拼接不严谨被人注入了恶意代码。还有一个容易踩的坑是性能。eval 每次都会重新编译字符串如果在循环里频繁调用性能会很差。这时候不如用 compile 预编译一下或者干脆换个思路用正则解析。和同类技术对比ast.literal_eval 是 eval 的安全替代品但它的局限性很明显——只能处理字面量。如果你需要执行算术运算或者字符串拼接它就不行了。所以 ast.literal_eval 适合解析配置数据比如从文件里读出来的字符串形式的列表。exec 函数和 eval 很像但 exec 能执行多行代码和语句。区别在于exec 没有返回值。如果你需要动态执行一段完整的代码比如用户定义的算法逻辑那就用 exec。但 exec 的风险比 eval 更大因为没有返回值反而让人更容易忽略安全问题。还有一个比较小众但值得提的选项是使用 operator 模块。比如我要动态执行一个加法可以用 operator.add 来替代 eval。虽然写起来麻烦点但完全避免了代码注入的问题。实际上很多需要动态执行表达式的场景都可以用字典映射的方式来解决operations{:operator.add,-:operator.sub,*:operator.mul,}resultoperations[](3,4)# 7这种方式更可控也更安全。只不过新手上手会觉得麻烦但其实写多了就会发现这种显式的映射反而让代码更好维护。说到底eval 是个很妖的玩意儿。用得好可以省很多力但稍不注意就会踩坑。我的建议是如果能用安全的方式实现就别碰 eval。但如果你确实需要它那就把限制做严把测试写全然后祈祷没有人往你的输入框里塞恶意代码。好的Python代码几乎没有区别。这在某些需要动态生成大量SQL查询的数据库驱动中得到了体现。总体而言exec是一个存在即合理的工具。不用刻意回避它但也不要滥用。当遇到动态代码执行的场景时认真考虑安全性、代码可维护性和替代方案然后再决定是否使用它。有时候用exec写几十行代码能解决的问题如果用其他方式可能需要写几百行而且更不清晰。