跳到主要内容

eval 函数与字符串解析

eval 函数是 Python 中一个强大但危险的工具。它能将字符串作为 Python 代码执行,这既是它的"魔法"所在,也是潜在的安全隐患。本章深入探讨 eval 的原理、使用场景和安全替代方案。

问题背景

m = "1,2,3"

# 写法 1:eval 方式
a = eval("[" + m + "]")
print(a) # [1, 2, 3]

# 写法 2:直接列表
b = ["1,2,3"]
print(b) # ['1,2,3']

# 写法 3:错误尝试
c = [" + m + "]
print(c) # [' + m + ']

为什么三种写法结果不同?核心在于字符串层级代码层级的区别。

eval 函数原理

eval(expression) 将字符串作为 Python 表达式解析并执行:

eval("1 + 2") # 3 —— 算术表达式
eval("[1, 2, 3]") # [1, 2, 3] —— 列表字面量
eval("max(1, 2, 3)") # 3 —— 函数调用

eval 会把字符串二次解析为 Python 代码,这是它的"魔法"也是危险所在。

为什么三种写法结果不同?

写法 1:eval("[" + m + "]")

m = "1,2,3"
eval("[" + m + "]")

执行过程:

  1. 字符串拼接"[" + "1,2,3" + "]""[1,2,3]"(字符串)
  2. eval 解析:把字符串 "[1,2,3]" 当作Python 代码解析
    • [] → 列表边界
    • 1, 2, 3 → 三个整数(逗号是分隔符)
  3. 返回:真实的 list 对象 [1, 2, 3]

写法 2:["1,2,3"]

a = ["1,2,3"]

Python 直接解析为列表字面量:

  • [] → 列表边界
  • "1,2,3" → 一个字符串元素(引号内整体是字符串)
  • 结果:['1,2,3'](列表里有 1 个字符串元素)

写法 3:[" + m + "]

c = [" + m + "]

Python 解析为列表字面量:

  • " + m + " → 一个字符串元素(引号内的 m 是字符,不是变量)
  • 结果:[' + m + ']

类型对比

写法结果元素类型元素数量
eval("[" + m + "]")[1, 2, 3]int3
["1,2,3"]['1,2,3']str1
[" + m + "][' + m + ']str1

eval 的安全风险 ⚠️

eval 的最大问题是它会执行任意 Python 代码。如果传入的字符串来自用户输入,可能导致严重的安全漏洞:

# 用户输入:1] + __import__('os').system('rm -rf /') + [1
# 拼接后:[1] + __import__('os').system('rm -rf /') + [1]
# eval 会执行系统命令!

:::danger 安全警告 禁止用 eval 处理用户输入!恶意用户可以通过构造特殊字符串来执行任意代码,包括删除文件、获取系统信息等危险操作。 :::

安全替代方案

方案 1:split() + 类型转换(推荐)

对于简单的数据格式,使用 split() 配合类型转换是最安全、最清晰的方式:

m = "1,2,3"

# 方案 1:split() + int()(推荐)
a = [int(x) for x in m.split(",")]
print(a) # [1, 2, 3]

# 处理浮点数
m2 = "1.5,2.7,3.9"
a2 = [float(x) for x in m2.split(",")]
print(a2) # [1.5, 2.7, 3.9]

方案 2:ast.literal_eval(仅解析字面量)

ast.literal_evaleval 的安全版本,它只解析 Python 字面量(字符串、数字、列表、字典、元组、布尔值、None),不执行任意代码:

import ast

m = "1,2,3"
a = list(ast.literal_eval(m))
print(a) # [1, 2, 3]

# 支持更复杂的字面量
data = '{"name": "Alice", "age": 25}'
obj = ast.literal_eval(data)
print(obj) # {'name': 'Alice', 'age': 25}

# 不会执行函数调用
# ast.literal_eval("print('hello')") # ValueError

:::tip ast.literal_eval vs eval ast.literal_evaleval 安全,因为它只解析字面量,不执行函数调用或表达式求值。但它仍然能解析嵌套的字面量结构,是处理配置数据等场景的好选择。 :::

方案 3:json.loads(处理 JSON 格式)

如果数据符合 JSON 格式,使用 json.loads 是最佳选择:

import json

data = '[1, 2, 3]'
a = json.loads(data)
print(a) # [1, 2, 3]

data2 = '{"name": "Alice", "age": 25}'
obj = json.loads(data2)
print(obj) # {'name': 'Alice', 'age': 25}

eval 的合理使用场景

虽然有安全风险,但 eval 在某些场景下仍然有用:

场景 1:数学表达式计算器

def calculate(expression: str) -> float:
"""简单的数学表达式计算器。"""
# 限制只能包含数字和运算符
allowed_chars = set("0123456789.+-*/() ")
if not all(c in allowed_chars for c in expression):
raise ValueError("不支持的字符")

return eval(expression)


print(calculate("1 + 2 * 3")) # 7
print(calculate("(1 + 2) * 3")) # 9

场景 2:动态配置(内部使用)

# 内部配置文件,非用户输入
config_str = """{
"debug": True,
"timeout": 30,
"threshold": 0.8
}"""

config = eval(config_str)
print(config["debug"]) # True

场景 3:代码生成与测试(谨慎使用)

:::warning exec() 的风险 exec()eval() 更危险,因为它可以执行任意语句(包括赋值、导入、循环等)。在生产代码中应尽量避免使用。 :::

# 生成测试用例(仅用于演示,生产代码不推荐)
for i in range(1, 6):
code = f"def test_{i}(): return {i} * 2"
exec(code)

print(test_1()) # 2
print(test_3()) # 6

# ✅ 推荐方案:使用函数工厂模式替代 exec
def make_test_func(multiplier):
def test_func():
return multiplier * 2
return test_func

test_funcs = {f"test_{i}": make_test_func(i) for i in range(1, 6)}
print(test_funcs["test_1"]()) # 2

常见误区

误区 1:滥用 eval

# ❌ 错误:安全风险
user_input = input("输入:")
result = eval(user_input)

# ✅ 正确:按需解析
user_input = "1,2,3"
result = [int(x) for x in user_input.split(",")]

误区 2:混淆字符串和代码

# ❌ 错误:字符串不会自动变成代码
m = "1,2,3"
a = ["1,2,3"] # ['1,2,3'] 不是 [1, 2, 3]

# ✅ 正确:显式转换
a = [int(x) for x in m.split(",")] # [1, 2, 3]

误区 3:用 eval 处理 JSON

# ❌ 不推荐:JSON 和 Python 语法有差异
data = '{"name": "Alice", "age": 25}'
obj = eval(data) # 可能出错

# ✅ 推荐:使用 json 模块
import json
obj = json.loads(data)

实战:安全的配置解析器

import ast
from typing import Any


def safe_parse_config(config_str: str) -> Any:
"""安全解析配置字符串。"""
try:
return ast.literal_eval(config_str)
except (ValueError, SyntaxError):
raise ValueError(f"无效的配置格式: {config_str!r}")


def parse_list_of_ints(input_str: str, delimiter: str = ",") -> list[int]:
"""解析逗号分隔的整数列表。"""
parts = input_str.split(delimiter)
return [int(part.strip()) for part in parts if part.strip()]


# 测试
print(safe_parse_config('[1, 2, 3]')) # [1, 2, 3]
print(safe_parse_config('{"key": "value"}')) # {'key': 'value'}
print(parse_list_of_ints("10, 20, 30, 40")) # [10, 20, 30, 40]

# 安全测试
try:
safe_parse_config("__import__('os').system('rm -rf /')")
except ValueError as e:
print(f"安全拦截: {e}")

运行结果:

[1, 2, 3]
{'key': 'value'}
[10, 20, 30, 40]
安全拦截: 无效的配置格式: "__import__('os').system('rm -rf /')"

小结

  • eval 会把字符串二次解析为 Python 代码,这是它的"魔法"也是危险所在
  • 引号决定了语义:引号内的内容是字符串,引号外的内容是代码
  • 禁止用 eval 处理用户输入,改用 split() + 类型转换或 ast.literal_eval
  • ast.literal_eval 是安全的字面量解析器,适合处理配置数据
  • json.loads 是处理 JSON 格式数据的最佳选择

理解字符串与代码的边界,能让你写出更安全、更清晰的代码。下一章我们将学习 Python 的哈希机制。