← 返回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表达式,globals 和 locals 分别是全局和局部命名空间字典,用于限定执行环境。
基础用法
# 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() 仅适用于表达式,不能执行语句(如 import、def、class、if 等)。如果需要执行语句,必须使用 exec()。这实际上是 eval 的一种天然安全边界——虽然它并不能完全防止恶意代码。
eval 能做什么: 算术运算、列表/字典/集合推导式、条件表达式(x if cond else y)、lambda表达式、属性访问、函数调用、切片操作等。
eval 不能做什么: 赋值语句、import 语句、def/class 定义、for/while 循环、try/except、with 语句等。
三、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 的四原则
最小权限原则: 只开最小的权限,只给必需的命名空间。设置 __builtins__ = {} 后逐个添加白名单函数。
输入来源控制: 纯系统生成代码(无用户输入)> 开发者输入的配置 > 用户输入的表达式。严禁将用户输入直接传递给 exec/eval。
降级原则: 能不用就不用。在考虑使用 exec/eval 之前,先检查是否有标准库模块可以解决问题。
隔离原则: 如果需要执行不可信代码,应当在操作系统层面隔离(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 生态中成熟的安全替代方案。
学习路径建议
入门: 掌握 eval() 和 exec() 的基础用法,理解它们的区别和适用场景。
进阶: 深入学习 compile() 的三种模式和优化标志,理解 code object 的内涵。
安全: 研究 ast.literal_eval() 和 AST 安全校验器的实现,掌握动态代码的安全防护技术。
精通: 通过 ast.NodeTransformer 实现代码自动转换和重构工具,深入理解 Python 的编译原理。
拓展: 阅读 CPython 源码中 compile.c 和 ceval.c 的实现,理解动态代码执行的底层机制。
"动态代码执行是 Python 元编程的终极武器。它的存在提醒我们:Python 不仅仅是一门语言,更是一个可以在运行时自我反思和自我修改的灵活系统。理解它、敬畏它、正确地使用它,是进阶 Python 开发者的重要里程碑。"
延伸阅读
Python 官方文档:eval()、exec()、compile() 内置函数
Python 官方文档:ast 模块 - 抽象语法树
CPython 源码:Python/ceval.c - 字节码解释器核心
CPython 源码:Python/compile.c - 编译器实现
《Fluent Python》第 25 章:元编程
《Python Cookbook》第 9 章:元编程技术
PEP 578 - Python 运行时审计钩子系统
PEP 551 - Python 安全透明性支持