动态代码执行(eval/exec/compile)

Python进阶编程专题 · Python运行时动态代码执行技术

专题:Python进阶编程系统学习

关键词:Python, eval, exec, compile, 动态执行, AST, ast.literal_eval, 安全

一、概述:什么是动态代码执行

动态代码执行是Python语言最强大的特性之一,它允许程序在运行时动态地解析、编译和执行字符串形式的Python代码。这意味着你的程序可以在运行过程中"编写"并执行新的代码,从而实现高度的灵活性和可扩展性。Python提供了三个核心内置函数来实现这一能力:eval()exec()compile()

虽然动态代码执行的能力极为强大,但它也带来了严重的安全风险,尤其是当执行的代码来自不可信的输入源时。因此,理解每种机制的工作原理、适用场景以及安全限制措施,是每一位Python进阶开发者必须掌握的关键技能。

核心概念:动态代码执行本质上绕过了Python编译期的静态分析和优化步骤,在运行时将字符串直接送入解释器处理。这打破了"代码即数据"的传统界限,使得代码和数据之间的边界变得模糊。

三者对比总览

函数 用途 返回值 支持语句 典型场景
eval() 求值表达式 有(表达式的值) 仅表达式 数学计算、条件判断
exec() 执行语句块 None 任意语句 动态执行脚本、加载配置
compile() 编译代码 code object 根据模式决定 反复执行同一段代码、性能优化

版本提示:在Python 3中,eval()exec() 都是内置函数(Python 2中 exec 是语句)。本文所有示例基于Python 3.8+,部分特性需要Python 3.10+。

二、eval() 表达式求值

eval() 函数用于执行一个字符串表达式,并返回表达式的值。它的语法为 eval(expression, globals=None, locals=None)。其中,expression 是一个字符串形式的Python表达式,globalslocals 分别是全局和局部命名空间字典,用于限定执行环境。

基础用法

# eval() 基础示例 result = eval("1 + 2 * 3 - 4 / 2") print(result) # 输出: 5.0 # 使用变量 x = 10 y = 20 result = eval("x * y + 100") print(result) # 输出: 300 # eval 支持列表推导等表达式语法 data = [1, 2, 3, 4, 5] result = eval("[n**2 for n in data if n % 2 == 0]") print(result) # 输出: [4, 16]

指定命名空间

# 通过 globals 和 locals 限制可见变量 class SafeCalculator: def __init__(self): self.allowed = {"abs": abs, "round": round, "max": max, "min": min} def evaluate(self, expr, **vars): env = {"__builtins__": {}} env.update(self.allowed) env.update(vars) return eval(expr, env) calc = SafeCalculator() print(calc.evaluate("abs(x - y)", x=10, y=30)) # 20 print(calc.evaluate("max(a, b, c)", a=5, b=9, c=3)) # 9

不安全示例:默认情况下 eval() 可以访问所有内置函数和导入的模块,这可能导致严重的安全漏洞。

# 危险!不限制命名空间的 eval user_input = "__import__('os').system('rm -rf /')" eval(user_input) # 这会执行系统命令! # 也可以读取文件: user_input = "open('/etc/passwd').read()" data = eval(user_input)

eval() 仅适用于表达式,不能执行语句(如 importdefclassif 等)。如果需要执行语句,必须使用 exec()。这实际上是 eval 的一种天然安全边界——虽然它并不能完全防止恶意代码。

eval 能做什么:算术运算、列表/字典/集合推导式、条件表达式(x if cond else y)、lambda表达式、属性访问、函数调用、切片操作等。

eval 不能做什么:赋值语句、import 语句、def/class 定义、for/while 循环、try/exceptwith 语句等。

三、exec() 动态执行

exec() 函数用于动态执行Python代码(可以包含任意语句)。它的语法为 exec(object, globals=None, locals=None)。与 eval() 不同,exec() 不返回值(始终返回 None),但可以通过命名空间参数获取执行后的变量状态。

基础用法

# exec() 基础示例 - 执行多行代码 code = """ import math def circle_area(radius): return math.pi * radius ** 2 r = 5 result = circle_area(r) print(f"半径为{r}的圆面积为: {result:.2f}") """ exec(code) # 输出: 半径为5的圆面积为: 78.54

通过命名空间获取执行结果

# 使用命名空间字典捕获执行结果 namespace = {} code = """ total = 0 for i in range(1, 101): total += i """ exec(code, namespace) print(namespace["total"]) # 输出: 5050 # 多变量捕获 ns = {} exec("a = [x**2 for x in range(10) if x % 2 == 0]", ns) print(ns["a"]) # 输出: [0, 4, 16, 36, 64]

动态创建函数和类

# exec 可以动态定义函数 def create_function(func_name, body): code = f""" def {func_name}(x, y): return {body} """ ns = {} exec(code, ns) return ns[func_name] add = create_function("add", "x + y") mul = create_function("mul", "x * y") power = create_function("power", "x ** y") print(add(3, 4)) # 7 print(mul(3, 4)) # 12 print(power(3, 4)) # 81
# exec 也可以动态创建类 code = """ class Person: species = "Homo sapiens" def __init__(self, name, age): self.name = name self.age = age def introduce(self): return f"Hi, I'm {self.name}, age {self.age}" @classmethod def get_species(cls): return cls.species """ ns = {} exec(code, ns) Person = ns["Person"] p = Person("Alice", 30) print(p.introduce()) # Hi, I'm Alice, age 30 print(Person.get_species()) # Homo sapiens

受限执行环境

# 通过 globals 限制 exec 的权限 def restricted_exec(code_str): """创建一个完全受限的执行环境""" allowed_builtins = { "abs": abs, "bool": bool, "int": int, "float": float, "str": str, "list": list, "dict": dict, "tuple": tuple, "set": set, "len": len, "range": range, "enumerate": enumerate, "zip": zip, "map": map, "filter": filter, "max": max, "min": min, "sum": sum, "sorted": sorted, "reversed": reversed, "print": print, "isinstance": isinstance, "hasattr": hasattr, "getattr": getattr, } safe_globals = {"__builtins__": allowed_builtins} safe_locals = {} try: exec(code_str, safe_globals, safe_locals) except Exception as e: return {"error": str(e)} return safe_locals result = restricted_exec(""" data = [1, 2, 3, 4, 5] result = sum(x**2 for x in data if x > 2) print(f"计算结果: {result}") """) print(result) # {'data': [1,2,3,4,5], 'result': 50}

四、compile() 编译优化

compile() 函数将字符串形式的源代码编译为一个代码对象(code object),随后可以反复传递给 exec()eval() 执行。它的语法为 compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)。预编译可以显著提升重复执行同一段代码的性能,因为编译步骤只需执行一次。

三种编译模式

模式说明配合执行函数
'eval'编译单个表达式eval()
'exec'编译语句块(任意多行)exec()
'single'编译单个交互式语句(可包含表达式和语句混合)exec()
# compile() 基础使用 - eval 模式 expr = "2 * pi * r" code_obj = compile(expr, "<string>", "eval") # 反复执行,只编译一次 pi = 3.14159 for r in [1, 2, 3, 4, 5]: result = eval(code_obj) print(f"r={r}: circumference={result:.2f}")
# compile() - exec 模式 code_str = """ def factorial(n): if n <= 1: return 1 return n * factorial(n - 1) for i in range(1, 11): print(f"{i}! = {factorial(i)}") """ compiled = compile(code_str, "<dynamic>", "exec") exec(compiled) # 执行编译后的代码对象
# compile() - single 模式(模拟交互式解释器) # single 模式适合逐条执行的交互式环境 lines = [ "x = 42", "y = x * 2", "print(f'x={x}, y={y}')", ] ns = {} for line in lines: code_obj = compile(line, "<interactive>", "single") exec(code_obj, ns)

使用 AST 优化标志

import ast # compile() 支持从 AST 节点直接编译 tree = ast.parse("x ** 2 + y ** 2", mode="eval") code_obj = compile(tree, "<ast>", "eval") result = eval(code_obj) print(result) # 需要 x 和 y 在作用域中 # 性能对比:预编译 vs 直接 eval import timeit expr = "sum(x**2 for x in range(100))" # 方法1:每次都重新解析编译 t1 = timeit.timeit('eval("sum(x**2 for x in range(100))")', number=10000) # 方法2:预编译后执行 compiled_expr = compile(expr, "<string>", "eval") t2 = timeit.timeit("eval(compiled_expr)", globals={"compiled_expr": compiled_expr}, number=10000) print(f"无编译: {t1:.4f}s") print(f"预编译: {t2:.4f}s") print(f"加速比: {t1/t2:.2f}x") # 预编译通常有 2-5 倍的性能提升

最佳实践:当需要在热路径(hot path)中反复执行同一段动态代码时,应当使用 compile() 预编译代码对象,然后将 code object 缓存起来反复使用。这可以避免每次重复的词法分析和语法分析开销。

compile() 的高级标志

import ast # 使用 PyCF_ONLY_AST 标志获取 AST 而非 code object tree = compile("x + y * z", "<string>", "eval", flags=ast.PyCF_ONLY_AST) print(ast.dump(tree, indent=2)) # 输出: AST 结构树 # optimize 参数控制优化级别 # 0: 无优化(默认) # 1: 移除 assert 语句 # 2: 移除 assert 和 __doc__ 文档字符串 code_opt0 = compile("assert 1 == 2", "<string>", "exec", optimize=0) code_opt1 = compile("assert 1 == 2", "<string>", "exec", optimize=1) exec(code_opt0) # 触发 AssertionError exec(code_opt1) # 无任何输出,assert 被优化掉了

五、安全风险与防护措施

动态代码执行最大的安全挑战在于:如果将用户输入直接传递给 eval()exec(),攻击者可以执行任意Python代码,包括系统命令、文件读写、网络访问等危险操作。以下是一些典型的攻击向量和防护策略。

黄金法则:永远不要使用 eval()exec() 处理来自不可信来源的输入!即使限制了 __builtins__,仍然可能存在绕过方式。

常见攻击向量

# 攻击向量1: __builtins__ 绕过 # 即使设置了 __builtins__ = {} cmd = "(lambda: (__import__('os').system('dir')))()" # 通过 ()() 构造 lambda + 调用即可绕过限制 # 攻击向量2: 通过类继承链访问危险函数 payload = """ [x for x in ().__class__.__bases__[0].__subclasses__() if x.__name__ == 'BuiltinImporter'][0] .load_module('os').system('dir') """ # 攻击向量3: 通过 getattr + chr 构造绕过 payload = """ __builtins__.__dict__['ZXBvcnQ='.decode('base64')] """

多层防护策略

import re class SafeEvalWithGuard: """ 多层防护的 eval 封装 策略: 输入清洗 + 内置函数白名单 + AST 安全检查 """ def __init__(self): self.allowed_names = { "abs": abs, "max": max, "min": min, "round": round, "sum": sum, "len": len, "int": int, "float": float, "bool": bool, "str": str, "list": list, "dict": dict, "tuple": tuple, "set": set, "range": range, "True": True, "False": False, "None": None, } # 禁止的语法模式正则 self.blacklist_patterns = [ r"__\w+__", # 禁止双下划线属性 r"import\s*\(?", # 禁止 import r"lambda", # 禁止 lambda(避免 ()() 调用模式) r"\.system\s*\(", # 禁止调用 system r"\.popen\s*\(", # 禁止 popen r"\.subprocess", # 禁止 subprocess r"open\s*\(", # 禁止 open ] def _check_input(self, expr): """第一层防护: 正则过滤危险模式""" for pattern in self.blacklist_patterns: if re.search(pattern, expr): raise ValueError(f"检测到危险语法: {pattern}") def evaluate(self, expr, **vars): """安全求值入口""" self._check_input(expr) env = {"__builtins__": {}} env.update(self.allowed_names) env.update(vars) try: return eval(expr, env) except Exception as e: raise ValueError(f"执行出错: {e}") # 使用示例 guard = SafeEvalWithGuard() print(guard.evaluate("max(a, b, c)", a=10, b=20, c=30)) # 30 print(guard.evaluate("int(x) * 2", x="21")) # 42 # 以下调用会触发 SecurityError # guard.evaluate("__import__('os').system('dir')")

安全要点总结:

1. 设置 __builtins__ = {} 移除所有内置函数,然后逐个添加需要的白名单函数。

2. 使用正则或 AST 分析对输入进行预处理过滤。

3. 优先使用 ast.literal_eval() 替代 eval(),如果只需要解析字面量的话。

4. 在沙箱环境(如 Docker 容器、subprocess 隔离进程)中执行动态代码。

5. 对执行时间进行限制,防止拒绝服务攻击(如 while True: pass)。

6. 记录和审计所有动态代码执行操作。

六、ast.literal_eval 安全替代方案

ast.literal_eval()eval() 的安全替代品,它只解析Python字面量结构:字符串、字节串、数字、布尔值、None、元组、列表、字典、集合以及它们的嵌套组合。与 eval() 不同,它不会执行任意代码,因此不会带来安全风险。

支持的数据类型

import ast # 安全的字面量解析 print(ast.literal_eval("42")) # 42 print(ast.literal_eval("'hello world'")) # hello world print(ast.literal_eval("[1, 2, 3, 4]")) # [1, 2, 3, 4] print(ast.literal_eval("{'a': 1, 'b': 2}")) # {'a': 1, 'b': 2} print(ast.literal_eval("(1, 2, 3)")) # (1, 2, 3) print(ast.literal_eval("{1, 2, 3}")) # {1, 2, 3} print(ast.literal_eval("True")) # True print(ast.literal_eval("None")) # None print(ast.literal_eval("b'hello'")) # b'hello' # 嵌套结构完全支持 data = ast.literal_eval(""" { "name": "Python", "version": (3, 10, 0), "features": ["dynamic", "interpreted", "OOP"], "stats": {"lines": None, "files": 42} } """) print(data) # {'name': 'Python', 'version': (3, 10, 0), # 'features': ['dynamic', 'interpreted', 'OOP'], # 'stats': {'lines': None, 'files': 42}}
# ast.literal_eval 会安全拒绝所有非字面量表达式 try: ast.literal_eval("__import__('os')") except ValueError as e: print(f"拒绝执行: {e}") try: ast.literal_eval("1 + 2") # 算术运算也被拒绝! except ValueError as e: print(f"拒绝求值: {e}")

实际应用:安全配置文件解析

import ast import json def load_config_safe(filepath): """ 安全加载配置文件,支持 Python 字面量语法 比 JSON 更灵活(支持元组、集合、注释等) """ with open(filepath, "r", encoding="utf-8") as f: content = f.read() return ast.literal_eval(content) # 示例配置文件内容 config.txt: # { # "host": "localhost", # "port": 8080, # "debug": True, # "allowed_ips": ("192.168.1.1", "10.0.0.1"), # 元组安全 # "empty": None # }
推荐: ast.literal_eval
  • 只解析字面量,不执行代码
  • 安全可靠,无代码注入风险
  • 支持 Python 原生数据结构
  • 比 JSON 更灵活
不推荐: eval
  • 执行任意代码,极不安全
  • 即使限制 __builtins__ 也可绕过
  • 不适合处理不可信输入
  • 99% 的场景可以用其他方案替代

七、AST 抽象语法树操作

Python的 ast 模块提供了一种强大的中间层:在解析源代码字符串和最终编译执行之间,我们可以获取并操纵抽象语法树。这使得开发者可以在代码执行之前进行静态分析、安全检查、代码转换和优化。

解析与遍历 AST

import ast # 解析源代码为 AST 树 code = """ def greet(name: str) -> str: \"\"\"向名字问好\"\"\" return f"Hello, {name}!" """ tree = ast.parse(code) print(ast.dump(tree, indent=2)) # 输出完整的 AST 树结构 # 使用 NodeVisitor 遍历 AST class FunctionAnalyzer(ast.NodeVisitor): def __init__(self): self.functions = [] self.calls = [] def visit_FunctionDef(self, node): self.functions.append({ "name": node.name, "args": [arg.arg for arg in node.args.args], "lineno": node.lineno, }) self.generic_visit(node) def visit_Call(self, node): if isinstance(node.func, ast.Name): self.calls.append({ "name": node.func.id, "lineno": node.lineno, }) self.generic_visit(node) analyzer = FunctionAnalyzer() analyzer.visit(tree) print("定义的函数:", analyzer.functions) print("调用的函数:", analyzer.calls)

AST 安全校验器

import ast class CodeSafetyValidator(ast.NodeVisitor): """ AST 级别的代码安全检查器 在 eval/exec 之前对代码进行静态分析 """ def __init__(self): self.unsafe_nodes = [] self.imports = [] def visit_Import(self, node): for alias in node.names: self.imports.append(alias.name) self.unsafe_nodes.append(("import", node.lineno)) self.generic_visit(node) def visit_ImportFrom(self, node): module = node.module or "" for alias in node.names: self.imports.append(f"{module}.{alias.name}") self.unsafe_nodes.append(("import_from", node.lineno)) self.generic_visit(node) def visit_Call(self, node): # 检查危险的函数调用 dangerous = {"eval", "exec", "compile", "open", "execfile", "input"} if isinstance(node.func, ast.Name): if node.func.id in dangerous: self.unsafe_nodes.append((f"dangerous_call:{node.func.id}", node.lineno)) elif isinstance(node.func, ast.Attribute): if node.func.attr in {"system", "popen", "call", "Popen", "run"}: self.unsafe_nodes.append((f"dangerous_attr_call:{node.func.attr}", node.lineno)) self.generic_visit(node) def visit_Attribute(self, node): # 检查双下划线属性访问 if isinstance(node.attr, str) and node.attr.startswith("__"): self.unsafe_nodes.append((f"dunder_access:{node.attr}", node.lineno)) self.generic_visit(node) def is_code_safe(code_str): """检查代码字符串是否安全可执行""" try: tree = ast.parse(code_str) except SyntaxError: return False, ["语法错误"] validator = CodeSafetyValidator() validator.visit(tree) if validator.unsafe_nodes: return False, validator.unsafe_nodes return True, [] # 测试 safe, issues = is_code_safe("x = [1, 2, 3]; print(sum(x))") print(f"安全: {safe}, 问题: {issues}") safe, issues = is_code_safe("__import__('os').system('dir')") print(f"安全: {safe}, 问题: {issues}")

AST 代码转换实战

import ast class NamePrefixTransformer(ast.NodeTransformer): """ 将代码中所有变量名前加上指定前缀 演示 AST 节点转换的能力 """ def __init__(self, prefix): self.prefix = prefix self.ignore = {"True", "False", "None"} def visit_Name(self, node): if isinstance(node.ctx, (ast.Load, ast.Store)) \ and node.id not in self.ignore: return ast.copy_location( ast.Name(id=f"{self.prefix}{node.id}", ctx=node.ctx), node ) return node # 应用转换 code = "result = x + y * z" tree = ast.parse(code, mode="exec") transformer = NamePrefixTransformer("my_") transformed_tree = transformer.visit(tree) ast.fix_missing_locations(transformed_tree) # 编译并执行转换后的代码 compiled = compile(transformed_tree, "<transformed>", "exec") ns = {"my_x": 10, "my_y": 20, "my_z": 5} exec(compiled, ns) print(ns["my_result"]) # 110

AST 应用场景:

代码静态分析(linting)和复杂度评估。安全审计和沙箱实现。代码自动转换(如 pytest 的断言重写)。代码生成和元编程。Python 重构工具(如 2to3、autopep8)。装饰器和上下文管理器的高级实现。

八、动态代码的实际应用

应用1:配置热加载系统

import os import time import hashlib class HotReloadConfig: """ 支持热加载的配置系统 配置文件为 Python 语法,exec 动态执行 """ def __init__(self, config_path): self.config_path = config_path self._config = {} self._last_mtime = 0 self._last_hash = "" self.load() def load(self): """加载配置文件到受限命名空间""" safe_builtins = { "True": True, "False": False, "None": None, "int": int, "float": float, "str": str, "bool": bool, "list": list, "dict": dict, "tuple": tuple, "os": os, # 显式允许 os.path 访问 } namespace = {"__builtins__": safe_builtins} with open(self.config_path, "r", encoding="utf-8") as f: code = f.read() exec(code, namespace) # 移除内部变量,只保留大写开头的配置项 self._config = {k: v for k, v in namespace.items() if k[0].isupper() or k.isupper()} return self._config def reload_if_changed(self): """检测文件变更并重载""" mtime = os.path.getmtime(self.config_path) if mtime > self._last_mtime: self._last_mtime = mtime return self.load() return None def __getitem__(self, key): return self._config.get(key) # 使用示例 # config.py 内容: # DATABASE_HOST = "localhost" # DATABASE_PORT = 5432 # DEBUG_MODE = True # ALLOWED_HOSTS = ["example.com", "api.example.com"] # config = HotReloadConfig("config.py") # print(config["DATABASE_HOST"]) # localhost # time.sleep(10) # config.reload_if_changed() # 检测到变更则热加载

应用2:简易 REPL 实现

import sys import traceback class MiniREPL: """ 微型交互式解释器 演示 eval/exec/compile 的综合应用 """ def __init__(self, banner="MiniREPL v1.0"): self.namespace = {"__builtins__": __builtins__} self.history = [] self.banner = banner def run(self): print(self.banner) while True: try: line = input(">>> ") if line.strip() in {"exit", "quit"}: break self.history.append(line) self._execute(line) except EOFError: break except KeyboardInterrupt: print("\nKeyboardInterrupt") continue def _execute(self, line): """智能判断是表达式还是语句""" try: # 先尝试作为表达式求值 code = compile(line, "<repl>", "eval") result = eval(code, self.namespace) if result is not None: print(repr(result)) except SyntaxError: # 不是表达式,尝试作为语句执行 try: code = compile(line, "<repl>", "exec") exec(code, self.namespace) except Exception: traceback.print_exc() except Exception: traceback.print_exc() # MiniREPL().run()

应用3:模板引擎核心

import re class MiniTemplateEngine: """ 微型模板引擎 使用 exec 和字符串替换实现模板渲染 """ def __init__(self): self.cache = {} def _compile_template(self, template_str): """将模板字符串编译为 Python 代码""" # 替换模板变量 {{ var }} 为 Python 表达式 # 替换模板语句 {% ... %} 为 Python 语句 code = '__result__ = []\n' # 转义字符串字面量中的特殊字符 escaped = re.sub(r"(['\\])", r"\\\1", template_str) # 分割模板 parts = re.split(r"(\{\{.*?\}\}|\{%.*?%\})", escaped) for part in parts: if part.startswith("{{") and part.endswith("}}"): expr = part[2:-2].strip() code += f'__result__.append(str({expr}))\n' elif part.startswith("{%") and part.endswith("%}"): stmt = part[2:-2].strip() if stmt.startswith("for") or stmt.startswith("while"): code += f"{stmt}\n" elif stmt.startswith("if") or stmt.startswith("elif") \ or stmt.startswith("else"): code += f"{stmt}:\n" elif stmt.startswith("end"): # 结束 for/if 块 pass else: code += f"__result__.append({stmt})\n" else: code += f'__result__.append(r"{part}")\n' code += '__result__ = "".join(__result__)\n' return compile(code, "<template>", "exec") def render(self, template_str, **context): """渲染模板""" if template_str not in self.cache: self.cache[template_str] = self._compile_template(template_str) code_obj = self.cache[template_str] context["__builtins__"] = {"range": range, "len": len, "str": str, "int": int} exec(code_obj, context) return context["__result__"] # 使用示例 engine = MiniTemplateEngine() template = """

{{ title }}

    {% for item in items %}
  • {{ item.name }}: {{ item.price }}元
  • {% endfor %}
"""
result = engine.render(template, title="商品列表", items=[ {"name": "苹果", "price": 5}, {"name": "香蕉", "price": 3}, {"name": "橘子", "price": 4}, ] ) print(result)

应用4:插件系统

import os import importlib.util class PluginSystem: """ 演示动态代码在插件系统中的应用 支持从文件或字符串动态加载插件 """ def __init__(self): self.plugins = {} def load_from_string(self, name, code_str): """从字符串动态加载插件""" namespace = {} exec(code_str, namespace) for attr_name, obj in namespace.items(): if callable(obj) or isinstance(obj, type): self.plugins[f"{name}.{attr_name}"] = obj return list(namespace.keys()) def load_from_file(self, filepath): """从文件动态加载插件""" name = os.path.splitext(os.path.basename(filepath))[0] # 方法1: 使用 exec with open(filepath, "r", encoding="utf-8") as f: code = f.read() namespace = {} exec(code, namespace) for attr_name, obj in namespace.items(): if not attr_name.startswith("_"): self.plugins[f"{name}.{attr_name}"] = obj return self.plugins def call(self, plugin_name, *args, **kwargs): if plugin_name not in self.plugins: raise KeyError(f"插件 '{plugin_name}' 未找到") return self.plugins[plugin_name](*args, **kwargs) def list_plugins(self): return list(self.plugins.keys()) # 使用示例:从字符串加载插件 plugin_system = PluginSystem() geometry_plugin = """ import math def circle_area(radius): return math.pi * radius ** 2 def rect_area(width, height): return width * height class Calculator: @staticmethod def triangle_area(base, height): return 0.5 * base * height """ plugin_system.load_from_string("geometry", geometry_plugin) print(plugin_system.call("geometry.circle_area", 5)) print(plugin_system.call("geometry.Calculator").triangle_area(4, 6))

九、性能考量与最佳实践

性能对比

使用 compile() 预编译代码对象可以显著提升重复执行的性能。以下是一组性能基准数据:

import timeit # 基准测试:直接 Python 代码 vs eval vs 预编译 eval setup = """ import math compiled = compile("math.sin(x) * math.cos(y)", "", "eval") """ # 1. 直接 Python 调用 t_direct = timeit.timeit( 'math.sin(1.5) * math.cos(2.5)', setup='import math', number=100000 ) # 2. eval 每次重新编译 t_eval = timeit.timeit( 'eval("math.sin(x) * math.cos(y)")', setup=setup + 'x=1.5; y=2.5', number=100000 ) # 3. 预编译后 eval t_compiled = timeit.timeit( 'eval(compiled)', setup=setup + 'x=1.5; y=2.5', number=100000 ) print(f"直接调用: {t_direct:.4f}s") print(f"eval(每次编译): {t_eval:.4f}s") print(f"eval(预编译): {t_compiled:.4f}s") print(f"预编译加速比: {t_eval/t_compiled:.2f}x") print(f"相比直接调用开销: {t_compiled/t_direct:.2f}x")

性能结论:即便使用预编译,动态代码执行仍然比直接编写的 Python 代码慢 5-20 倍。这是因为 eval(compiled_code) 仍然需要经过完整的字节码解释执行流程,而且相比静态编译的代码,解释器无法对 eval/exec 中的代码进行全局优化。因此在性能敏感的场景中应谨慎使用动态代码执行。

最佳实践清单

场景推荐方案原因
解析配置文件ast.literal_eval / JSON / YAML安全、简单、无需代码执行
计算字符串表达式ast.literal_eval(若为字面量)或受限 eval需要严格限制输入来源和命名空间
动态加载插件importlib / exec(文件来源必须可信)importlib 更符合 Python 标准机制
模板引擎Jinja2 / Mako 等专用库成熟方案,内置沙箱和优化
REPL 交互解释器code 模块 / compile(single) 模式标准库已有完善的实现
RPC 远程执行不建议使用 exec/eval安全风险极高,需专用协议
数学表达式numexpr / sympy 等专用库性能更好、功能更专一
热重载配置exec + 受限命名空间兼顾灵活性和安全性

使用 exec/eval 的四原则

  1. 最小权限原则:只开最小的权限,只给必需的命名空间。设置 __builtins__ = {} 后逐个添加白名单函数。
  2. 输入来源控制:纯系统生成代码(无用户输入)> 开发者输入的配置 > 用户输入的表达式。严禁将用户输入直接传递给 exec/eval。
  3. 降级原则:能不用就不用。在考虑使用 exec/eval 之前,先检查是否有标准库模块可以解决问题。
  4. 隔离原则:如果需要执行不可信代码,应当在操作系统层面隔离(Docker、VM 或 subprocess 独立进程)。
# 安全的 exec 封装(推荐模式) import contextlib import io import sys def safe_exec(code_str, allowed_names=None, timeout=None): """ 安全的 exec 封装 - 限制内置函数白名单 - 捕获标准输出 - 可选的执行时间限制 """ if allowed_names is None: allowed_names = {"print": print, "len": len, "range": range} _globals = {"__builtins__": allowed_names} _locals = {} # 捕获标准输出 stdout_capture = io.StringIO() with contextlib.redirect_stdout(stdout_capture): try: exec(code_str, _globals, _locals) except Exception as e: return {"error": str(e), "stdout": stdout_capture.getvalue()} return {"locals": _locals, "stdout": stdout_capture.getvalue()} # 使用 result = safe_exec(""" data = list(range(1, 11)) print(f"总和: {sum(data)}") avg = sum(data) / len(data) print(f"平均: {avg:.2f}") """) print(result["stdout"]) # 输出: 总和: 55 # 平均: 5.50 print(result["locals"]["avg"]) # 5.5

十、总结

核心要点回顾:

eval() 用于求值单个表达式并返回结果,适合简单的动态计算场景。

exec() 用于执行任意Python代码(语句块),适合动态加载配置、插件、模板等场景。

compile() 将源代码预编译为 code object,配合 eval/exec 使用可在循环中显著提升性能。

ast.literal_eval() 是 eval 的安全替代方案,用于安全解析Python字面量结构。

ast 模块提供代码的静态分析和转换能力,在代码执行前进行安全检查和自动重构。

动态代码执行是一把双刃剑:它提供了无与伦比的灵活性,但同时也引入了严重的安全风险。在绝大多数场景中,应当优先使用 Python 生态中成熟的安全替代方案。

学习路径建议

"动态代码执行是 Python 元编程的终极武器。它的存在提醒我们:Python 不仅仅是一门语言,更是一个可以在运行时自我反思和自我修改的灵活系统。理解它、敬畏它、正确地使用它,是进阶 Python 开发者的重要里程碑。"

延伸阅读