同一份参数检查,直接运行会报错,加上 -O 却放过非法值。这里用 Linux 上的 CPython 3.14.3 对照默认、-O 和 -OO,定位 assert 消失的原因,再把入口改为显式异常,核对拒绝与放行两条路径。
一、先分清报错消失和问题解决
例如数量必须是正整数,入口用断言挡住零。启动参数一变,零却走到后面。这不是输入自动变合法,也不是异常被修复。解释器可能根本没有生成那条断言对应的代码。先核对实际启动命令,再看条件表达式,比追着日志猜原因有效。
本例只验证本机语言行为和命令行退出结果,不连接网络,不修改生产配置。它不是一次真实 Web 请求、授权检查或生产发布验收。拒绝样本得到报错,只能证明这条测试路径按预期拒绝,不能证明业务已经安全。
二、assert 是调试断言,不是必经关卡
官方把 assert 定义为插入调试断言的便捷方式。通常运行时,条件为假会抛出 AssertionError;带消息的形式还会计算消息表达式。条件为真时,消息不会求值。因此,不能靠消息函数承担正常流程里的操作。
在这里的 CPython 实现中,优化编译会删除整条 assert,包括条件和消息表达式。把计数、写文件、扣减库存、提交记录藏在里面,优化后这些动作都可能不发生。断言应描述供开发期检查的内部假设,不负责业务参数校验、权限授权或必需的写入副作用。
三、启动选项影响的是编译行为
默认情况下 __debug__ 为真;请求优化时为假。-O 会移除断言及依赖 __debug__ 的调试代码,-OO 在此基础上还移除文档字符串。不要把必需的校验放进 if __debug__ 分支。__debug__ 也不是一个可以通过赋值切换的普通开关。
优化选项不保证更快。出现差异时,同时检查启动器、容器命令和环境中的 PYTHONOPTIMIZE。环境变量也能启用优化,进程参数中不必出现 -O。

四、用同一解释器跑三个真实子进程模式
下面完整代码保存为“随文实验.py”,用自己的 Python 3 运行。只用标准库,以 sys.executable 启动真实子进程,不靠修改父进程标志模拟优化。临时源码放在临时目录,退出自动清理;测试不接触业务文件。
复制当前环境后删除全部 PYTHON 开头的变量,避免优化级别、模块路径等继承值污染对照。临时路径由 tempfile.gettempdir() 取得,TMPDIR 只设置在复制的环境里,不修改系统环境。本机运行前指定了隔离的 scratch 目录;读者可使用自己的可写临时目录。
import json, os, subprocess, sys, tempfile
from pathlib import Path
环境 = {键: 值 for 键, 值 in os.environ.items()
if not 键.startswith("PYTHON")}
环境["TMPDIR"] = tempfile.gettempdir()
Path(环境["TMPDIR"]).mkdir(parents=True, exist_ok=True)
def 检查(成立, 说明):
if not 成立:
raise RuntimeError(说明)
# 子进程执行的源码,不在父进程里模拟优化。
源码 = '''import json, sys
动作, 数量 = sys.argv[1], json.loads(sys.argv[2])
if 动作 == "断言":
assert False, "拒绝"
if 动作 == "副作用":
事件 = []
def 条件():
事件.append("条件")
return False
def 消息():
事件.append("消息")
return "拒绝"
try:
assert 条件(), 消息()
except AssertionError:
pass
print(json.dumps(事件, ensure_ascii=False))
if 动作 == "校验":
try:
if type(数量) is not int:
raise TypeError("必须是整数且不是布尔值")
if 数量 <= 0:
raise ValueError("必须为正数")
except (TypeError, ValueError) as 错误:
print(str(错误), file=sys.stderr)
sys.exit(2)
print("接受")
'''
用例 = [("断言", 0), ("副作用", 0)]
用例 += [("校验", 值) for 值 in (False, 0, -1, "3", 3)]
通过 = 0
with tempfile.TemporaryDirectory(dir=环境["TMPDIR"]) as 目录:
文件 = Path(目录) / "子程序.py"
文件.write_text(源码, encoding="utf-8")
for 参数 in ([], ["-O"], ["-OO"]):
for 动作, 值 in 用例:
结果 = subprocess.run(
[sys.executable, *参数, str(文件), 动作, json.dumps(值)],
env=环境, cwd=目录, capture_output=True,
text=True, encoding="utf-8", timeout=10)
if 动作 == "断言":
检查(结果.returncode == (0 if 参数 else 1), "断言退出码")
检查(("AssertionError: 拒绝" in 结果.stderr) == (not 参数), "断言诊断")
elif 动作 == "副作用":
检查(结果.returncode == 0, "副作用进程失败")
检查(json.loads(结果.stdout) == ([] if 参数 else ["条件", "消息"]), "副作用")
else:
合法 = type(值) is int and 值 > 0
检查(结果.returncode == (0 if 合法 else 2), "校验退出码")
检查(结果.stdout == ("接受\n" if 合法 else ""), "校验输出")
检查(bool(结果.stderr) == (not 合法), "错误传播至命令行")
通过 += 1
print(参数 or ["默认"], 动作, repr(值), "通过")
print("通过用例:", 通过)
子程序提供断言、副作用、显式校验三个入口。副作用入口故意在条件和消息里记事件,只用于观察“整句删除”,不能照搬为实际写入设计。数量通过 JSON 传入,避免把所有输入都当作字符串。
每种模式跑同一组样本,父进程收集退出码、标准输出和标准错误。超时也会让实验失败,不把卡住当成通过。父进程的检查使用显式判断,即使实验本身被优化启动,也不能靠会消失的断言判定结果。
默认模式下,必假断言应非零退出,并包含对应异常;另外两种模式直接跳过。事件列表应从“条件、消息”变成空列表。这里捕获断言只为了观察记录,不代表业务入口应该吞掉异常。
校验入口则相反:三个模式都必须拒绝非法值,且不能打印“接受”。合法正整数需要正常返回,避免把“所有输入都报错”错算成修复。最后统计的是样本数,不是性能分数。
五、显式异常要配合明确的输入策略
示例先判断类型,再判断范围。这里策略是只收内置 int,且大于零。bool 是 int 的子类,所以 isinstance(值, int) 会接受布尔值;使用 type(值) is int 才与本例策略一致,同时也会拒绝整数子类。若业务允许子类,必须重新定义边界,不能机械替换。
字符串“3”没有自动转整数,小数也不接受。是否转换、是否允许零、最大数量是多少,都属于业务规则。显式 if/raise 在三个模式仍执行,前提是控制流确实走到这里,并且异常没有被上层吞掉。只换关键字,漏掉类型或范围,仍然可能放过错误输入。
六、捕获之后,命令行仍须失败退出
异常可以直接向上传播,由命令行进程以非零状态结束。若为了友好提示而捕获,就应明确 sys.exit(2),不能打印一句错误后继续执行并返回零。调度器通常先看退出码;看到零可能把本次作业当成功,而不理解中文错误日志。
本机随文实验三种模式共二十一项通过。独立扩展测试另外覆盖未捕获异常、捕获退出、吞异常反例、正整数、布尔值及其他类型,还检查模块和函数文档字符串。它也验证 PYTHONOPTIMIZE=1、-E 忽略该环境值,以及 -E 不会取消显式 -O。后者不是“关闭优化”开关。
另一个语法坑是 assert (条件, 消息):括号内构成非空元组,即使条件为假,元组仍为真。正确的断言消息写法是 assert 条件, 消息。不过语法写对,也不能把它升级为业务守门。扩展测试还对照 compile 的显式优化级别,提醒编译入口同样可能改变断言行为。
七、按现象回查,不先改生产参数
| 现象 | 优先检查 | 处理方向 |
|---|---|---|
| 加 -O 后不拒绝 | 是否用 assert 或 __debug__ 守门 | 改必经的显式校验 |
| 没有 -O 仍跳过 | PYTHONOPTIMIZE 与实际解释器 | 清理实验环境后重现 |
| 计数或写入消失 | 操作是否藏在断言表达式里 | 移出断言,独立检查结果 |
| 日志报错却退出零 | 捕获后是否继续返回 | 命令行非零退出 |
| -OO 后帮助文本缺失 | 是否依赖文档字符串 | 核对启动模式与文档依赖 |
八、验收要同时看拒绝、放行和业务结果
- 默认、-O、-OO 使用同一解释器,记录实际版本与环境隔离方式。
- 非法类型和越界值全部拒绝,布尔值按明确策略处理;合法样本能够放行。
- 需要失败的命令行返回非零,不只检查错误文本,也检查没有输出成功标记。
- 真实系统还要核对请求状态、调用方处理、授权边界、写入和回滚结果。本实验没有覆盖这些业务验收。
修复目标不是强迫所有部署都关闭优化,而是让必要规则不依赖优化开关。内部调试仍可使用断言;对外承诺的参数、授权和写入路径,应由显式逻辑负责,并由实际调用链验证。
🔕 评论已关闭