Python的断言把我坑惨了,原来assert和raise的区别这么大

简介: 本文通过小许因误用`assert`做权限校验导致生产环境安全漏洞的案例,深入剖析`assert`与`raise`的本质区别:前者是开发自检工具,启用`-O`优化时会被完全移除;后者是正式错误处理机制,始终生效。强调业务逻辑(权限、参数、外部数据校验)必须用`raise`,不可依赖`assert`。

小许那天给一个内部管理系统加了个权限校验。

他的思路很简单:在修改用户信息的接口入口,检查当前登录用户是不是管理员。如果不是,就拒绝操作。他写下了这样一行代码:

def update_user(request, user_id, data):
   assert request.user.is_admin, "只有管理员才能修改用户信息"
   
   # 后续的修改逻辑
   save_user(user_id, data)

本地测试时,他用普通用户账号调接口,程序确实报了错:

AssertionError: 只有管理员才能修改用户信息

接口返回了 500,修改被拒绝了。测试通过,小许很满意。

上线之后,一切正常。直到有一天,一个普通用户通过这个接口改掉了别人的资料。小许接到投诉,第一反应是:“不可能啊,我写了assert,非管理员进不来。”

他重新读了自己的代码,又在本机跑了一遍,assert明明生效。他去查生产环境的启动命令,发现运维在启动脚本里加了一个 -O 参数:

python -O main.py

他当时不知道 -O 是什么意思。查了文档才明白:-O 会让 Python 运行在“优化模式”下,所有 assert 语句会被全部移除。他写的权限检查,在生产环境里根本不存在。

“我明明写了校验,为什么上线就没了?”

问题出在他把 assert 当成了错误处理工具。assert 的定位是开发者自检,不是业务逻辑校验。它可以在启动时被整体关闭,而 raise 不会。

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (12).png

先看一个最直观的对比

def check_age(age):
   assert age >= 0, "年龄不能为负"
   print(f"年龄: {age}")

def check_age_raise(age):
   if age < 0:
       raise ValueError("年龄不能为负")
   print(f"年龄: {age}")

check_age(25)        # 年龄: 25
check_age(-1)        # AssertionError: 年龄不能为负

check_age_raise(25)  # 年龄: 25
check_age_raise(-1)  # ValueError: 年龄不能为负

两者的表面行为很相似:条件不满足时,都会抛出一个异常。但它们的本质完全不同。

assert 是一个调试工具,用来确认“这里的情况一定符合我的假设”。如果不符合,说明代码有 bug,应该立刻停下来。

raise 是一个错误处理机制,用来在程序运行时主动报告一个错误。它属于程序的正常逻辑,无论是否优化模式,都会执行。

用一个生活场景来理解。

你在自己家里装了一个“煤气泄漏检测器”。

assert 的做法是:你在装修时贴了一张便签纸,上面写着“出门前必须关煤气”。这张便签是你给自己看的提醒,只有你自己记得看。如果你把它撕了,或者换了一个不看便签的人来住,提醒就失效了。

raise 的做法是:你装了一个正式的报警器,一旦检测到煤气泄漏,就大声报警,而且这个报警器是焊接在墙上的,谁也拆不掉。无论谁来住,报警器都在工作。

区别一目了然:assert 是开发者给自己贴的便签,可能被撕掉;raise 是正式安装的报警器,永远在线。

assert 的本质:开发者自检

assert 的语义是:“我确信这里的情况一定满足某个条件。如果不满足,说明我的代码写错了,请立刻告诉我。”

它适合用来检查只可能因为代码 bug 而违反的条件:

def calculate_average(numbers):
   assert len(numbers) > 0, "列表不能为空"
   return sum(numbers) / len(numbers)

这个函数的契约是“传入非空列表”。如果调用者传了空列表,说明调用者的代码有 bug,assert 帮你立刻发现。

assert 的另一个常见用途是检查内部不变量:

def process(data):
   assert isinstance(data, dict), "内部错误:data 应该是 dict"
   # ...

这类检查是给开发者自己看的,不是给最终用户看的。

raise 的本质:正式的错误报告

raise 的语义是:“现在发生了一个错误,我需要报告给上层调用者,让上层决定怎么处理。”

它适合用来处理运行时可能发生的、调用者应该知道的错误:

def withdraw(account, amount):
   if amount <= 0:
       raise ValueError("取款金额必须大于 0")
   if amount > account.balance:
       raise InsufficientFundsError("余额不足")
   account.balance -= amount

这些错误不是“代码 bug”,而是“业务逻辑中可能发生的情况”。调用者需要知道这些错误,并做出相应的处理。

关键区别一:-O 优化模式会移除所有 assert

这是 assert 和 raise 最本质、也最危险的差异。

Python 启动时如果加了 -O 参数,或者设置了环境变量 PYTHONOPTIMIZE,所有 assert 语句都会被移除。

# test.py
assert 1 == 2, "这行会报错"
print("程序继续")

正常运行:

$ python test.py
AssertionError: 这行会报错

优化模式运行:

$ python -O test.py
程序继续

assert 1 == 2 被直接删掉了,程序完全不知道曾经有过这行代码。

这个设计是有意为之的。assert 的定位是“开发阶段的检查”,在生产环境里,这些检查应该已经被验证过了,不需要再花时间执行。所以 Python 允许用 -O 把它们全部去掉。

但问题在于:很多人把 assert 当成了业务逻辑的一部分。权限检查、参数校验、数据完整性检查,全都用 assert 写。一旦生产环境用 -O 启动,这些检查全部消失,系统带着漏洞裸奔。

raise 没有这个问题。无论是否优化模式,raise 都会执行。

关键区别二:assert 只能抛 AssertionError

assert 只能抛出 AssertionError,没有其他选择。raise 可以抛出任何异常类型:

raise ValueError("值错误")
raise TypeError("类型错误")
raise KeyError("键不存在")
raise RuntimeError("运行时错误")
raise PermissionError("没有权限")
raise CustomError("自定义错误")

异常类型是调用者处理错误的重要依据。调用者可以这样写:

try:
   withdraw(account, amount)
except InsufficientFundsError:
   show_balance_warning()
except ValueError:
   show_input_error()

不同的异常类型走不同的处理分支。assert 只能抛 AssertionError,调用者无法区分“权限不足”和“参数为空”,只能笼统地捕获 AssertionError。

关键区别三:assert 不能带自定义异常信息之外的东西

assert 的第二个参数只能是一个表达式,通常是一个字符串:

assert x > 0, f"x 必须大于 0,当前值是 {x}"

这个字符串会成为 AssertionError 的 args。但它不能指定异常类型,不能附加结构化信息,不能链式关联其他异常。

raise 可以做得更多:

raise ValueError(f"x 必须大于 0,当前值是 {x}") from original_error

from 可以保留原始异常的上下文,调试时能看到完整的异常链。

坑一:用 assert 做参数校验

这是最常见的误用。

def create_user(username, age):
   assert username, "用户名不能为空"
   assert age >= 0, "年龄不能为负"
   # ...

用户输入校验是业务逻辑的一部分,不是开发者自检。用户可能输入空用户名,可能输入负数年龄,这是正常的运行时错误,应该用 raise:

def create_user(username, age):
   if not username:
       raise ValueError("用户名不能为空")
   if age < 0:
       raise ValueError("年龄不能为负")
   # ...

用 assert 的后果是:在 -O 模式下,所有校验消失,非法数据直接进入系统。

坑二:用 assert 做权限检查

这就是小许踩的坑。

def delete_post(request, post_id):
   assert request.user.is_admin, "只有管理员才能删除"
   # ...

权限检查是安全逻辑,绝对不能依赖 assert。因为 assert 可能被移除,权限检查就可能被绕过。

正确的做法:

def delete_post(request, post_id):
   if not request.user.is_admin:
       raise PermissionError("只有管理员才能删除")
   # ...

坑三:用 assert 检查外部数据

def parse_config(config):
   assert "host" in config, "配置缺少 host"
   assert "port" in config, "配置缺少 port"

配置文件可能来自外部,缺失字段是正常的运行时情况,不是代码 bug。应该用 raise,让调用者能够捕获并处理。

def parse_config(config):
   if "host" not in config:
       raise ConfigError("配置缺少 host")
   if "port" not in config:
       raise ConfigError("配置缺少 port")

坑四:用 assert 检查返回值

def fetch_data(url):
   response = requests.get(url)
   assert response.status_code == 200, "请求失败"
   return response.json()

HTTP 请求失败是正常的运行时情况,不是代码 bug。网络可能断开,服务器可能返回 500。应该用 raise:

def fetch_data(url):
   response = requests.get(url)
   if response.status_code != 200:
       raise HTTPError(f"请求失败: {response.status_code}")
   return response.json()

什么时候用 assert,什么时候用 raise?

判断标准很清晰:

条件只可能因为代码 bug 而违反:用 assert。

条件可能因为用户输入、外部数据、运行时环境而违反:用 raise。

内部不变量、算法前提条件、开发阶段的自检:用 assert。

参数校验、权限检查、业务规则、外部数据校验:用 raise。

测试代码里的断言:用 assert。测试本身就是开发阶段的检查,用 assert 完全合理。

生产代码里的错误处理:用 raise。

一个简单的判断方法:问自己“如果这行检查被删掉,程序还能不能正确运行?”如果答案是“不能”,那就不能用 assert,因为 assert 真的可能被删掉。

那 assert 还有用吗?

有,但用途很窄。

assert 最适合用来检查内部不变量和算法前提条件,而且这些检查在开发阶段有价值,在生产环境可以省略。

比如:

def binary_search(arr, target):
   assert arr == sorted(arr), "二分查找要求数组有序"
   # ...

这个断言检查的是调用者是否遵守了函数的契约。如果数组没排序,二分查找的结果是错的,这是调用者的 bug。开发阶段用 assert 能快速发现,生产环境省略也无所谓——如果调用者真的传了无序数组,程序会返回错误结果,但不会崩溃。

再比如:

def merge(left, right):
   assert isinstance(left, list) and isinstance(right, list)
   # ...

这类检查在开发阶段帮助发现类型错误,生产环境省略也不影响正常逻辑。

还有一种用法是在测试代码里:

def test_add():
   assert add(1, 2) == 3
   assert add(-1, 1) == 0

测试代码本身就是开发阶段的检查,assert 被优化掉也无所谓,因为测试只在开发环境跑。

性能上的差异

assert 在优化模式下会被移除,所以“没有性能开销”。raise 在正常运行时也不会有性能问题,因为异常只在出错时才会创建和抛出。

但要注意:assert 后面的表达式在非优化模式下会被求值。如果表达式有副作用,优化模式下副作用消失,行为会不一致:

assert do_something(), "失败了"  # do_something() 在 -O 模式下不会执行

永远不要在 assert 后面写有副作用的表达式。assert 只应该用来检查纯条件,不应该用来执行逻辑。

一个容易忽略的细节:assert 的第二个参数

assert 的第二个参数可以是任何表达式,但通常用字符串:

assert x > 0, f"x 必须大于 0,当前值是 {x}"

这个字符串在断言失败时会成为 AssertionError 的参数:

try:
   assert False, "出错了"
except AssertionError as e:
   print(e)  # "出错了"

如果第二个参数不是字符串,它会被原样作为 AssertionError 的参数:

assert False, {"code": 400, "msg": "错误"}

但这不太常见,也不推荐。assert 的异常信息应该是给人看的,不是给程序解析的。

回到小许的故事

小许最终的代码是这样的:

def update_user(request, user_id, data):
   if not request.user.is_admin:
       raise PermissionError("只有管理员才能修改用户信息")
   
   # 后续的修改逻辑
   save_user(user_id, data)

他把 assert 换成了 if + raise。权限检查不再依赖 -O 是否开启,无论生产环境怎么启动,检查都会执行。

他还顺手检查了项目里其他的 assert,发现了十几处用 assert 做参数校验、配置检查、返回值检查的地方。这些全部改成了 raise。

他后来在团队文档里写了一条:“assert 是给开发者自己看的,用来检查代码 bug,生产环境用 -O 启动时会被全部移除。raise 是给调用者看的,用来报告运行时错误,永远会执行。权限检查、参数校验、外部数据检查,一律用 raise,绝对不能用 assert。”

这个区分看起来简单,但真到写代码的时候,很多人会觉得 assert 写起来更短、更“优雅”,随手就用了。然后在生产环境被 -O 坑一次,才发现问题的严重性。

记住那个煤气报警器的比喻:assert 是贴在墙上的便签,可能被撕掉;raise 是焊死在墙上的报警器,永远在线。你要保护的是安全,不是开发者的记忆。

断言本身不难,难的是想清楚“这个检查是给谁看的、能不能被省略”。想清楚了,assert 和 raise 就不再是坑,而是两个职责分明的工具。

目录
相关文章
|
云安全 安全 网络安全
|
7天前
|
JavaScript 安全 前端开发
Nodejs Brotli decoder 代码执行漏洞 (CVE-2026-111497)10629
Node.js发布安全更新修复Brotli解码器高危RCE漏洞(CVE-2026-111497),建议受影响版本用户立即升级。
1997 1
|
14天前
|
JSON 程序员 数据格式
最新版Notepad++下载、安装、配置保姆级教程(附官网安装包)
Notepad++ 是一款免费开源、轻量快速的 Windows 文本编辑器,支持多标签、语法高亮、代码折叠与宏录制,体积小、启动快、资源占用低,兼容 x64/x32/ARM 架构,是程序员和文字工作者的高效利器。
2964 0
|
13天前
|
人工智能 API 开发工具
DeepSeek Harness 神插件:白嫖 13 家大厂模型
`dsh-our-free-model` 是 DeepSeek Harness 神级插件:零配置、零API Key、零账号,聚合13家大厂免费模型(含DeepSeek V4.1 Flash、Kimi K3等),支持本地网关、用量看板与自动降级,真正实现“白嫖即用”。
2203 44
|
10天前
|
Web App开发 人工智能 前端开发
2026 年 AI 浏览器怎么选?Ego Lite、Tabbit、Chrome DevTools MCP 深度对比,开发者到底该用哪个?
2026年AI浏览器选型指南:Ego Lite专注多Agent协同(Mac优先),Tabbit国产首选,原生支持Windows与Codex控制;Chrome DevTools MCP非浏览器而是开发者利器,深度集成Console/Network/Performance调试能力,让AI真正“看懂并验证”前端代码。
1398 3
|
7天前
|
安全 应用服务中间件 Go
PHP2Shell 漏洞拆解:WordPress 与 nginx 为何中招
PHP2Shell 双 RCE 已遭在野利用:WordPress、Drupal 全在射程内,nginx 其实另有洞。拆成因与修复顺序
PHP2Shell 漏洞拆解:WordPress 与 nginx 为何中招
|
7天前
|
开发者 索引
DeepSeekHarness大肥鱼自带插件市场了?加个地址就能用
Open Plugin Market 是一个开放插件索引平台,收录800+优质DSH插件,支持分类浏览、智能推荐与随机发现。无需安装市场,粘贴索引地址即可一键接入,网页版直访 dshopen.org,助力小开发者被看见。(239字)
1598 1
DeepSeekHarness大肥鱼自带插件市场了?加个地址就能用
|
6天前
|
人工智能 Linux API
Magpie, 轻量本地AI网关!免费Token用不完!
Magpie 是开源AI编程模型调度网关,上线一月获1.8万Star。它通过本地高性能网关统一托管多平台API密钥、账号与免费额度,实现跨Agent的模型秒级切换、静默额度轮换与资源复用,显著降低碎片化管理成本。(239字)
Magpie, 轻量本地AI网关!免费Token用不完!
|
6天前
|
安全 程序员 应用服务中间件
PHP 被曝两大致命漏洞,请所有程序员抓紧做这件事!
PHP 官方发布了紧急安全更新,一口气修复了两个严重漏洞。如果网站和服务器用了 PHP 应该怎么处理?漏洞的原理是什么?本文给你讲清楚
678 0

热门文章

最新文章