Python 的 assert 是干什么的?为什么生产环境不能用它做参数校验?
简化版
**assert 是「断言」——用来在开发/测试阶段检查「本该永远为真的条件」,如果条件为假就抛 AssertionError;它的核心用途是「捕捉程序员的 bug(不该发生的内部状态)」,不是做正常的业务校验。**语法:assert 条件, 可选错误信息——等价于 if not 条件: raise AssertionError(错误信息)。关键坑(也是它不能做生产校验的原因):Python 用 -O(optimize)参数运行时,所有 assert 会被整个移除(当作不存在)——python -O script.py 或设 PYTHONOPTIMIZE=1 时,断言不执行。所以:① 绝不能用 assert 做「用户输入校验」「权限检查」「关键业务逻辑」——生产环境一旦开 -O,这些检查全失效,安全漏洞或数据错误直接放行;② assert 只该用于「不变量检查」「防御性编程」「测试」——检查「如果这里为假说明我的代码有 bug」。正确的参数校验:用 if not condition: raise ValueError(...)(显式抛异常、不会被优化掉)。核心记忆:assert 检查「不该发生的 bug」,-O 会移除所有断言;参数/输入校验用 raise,绝不用 assert。
详细版
assert 的语义和坑:
| 方面 | 说明 |
|---|---|
| 语法 | assert 条件 或 assert 条件, 消息 |
| 等价于 | if not 条件: raise AssertionError(消息) |
| 条件为假 | 抛 AssertionError |
-O 运行 | 所有 assert 被移除、不执行 |
| 适用 | 不变量检查、防御性编程、测试 |
| 不适用 | 用户输入校验、权限检查、业务逻辑 |
# 基本用法
def average(nums):
assert len(nums) > 0, "列表不能为空" # 内部不变量
return sum(nums) / len(nums)
# 等价于
def average2(nums):
if not (len(nums) > 0):
raise AssertionError("列表不能为空")
return sum(nums) / len(nums)
# -O 会移除断言:
# python -O script.py 运行时,assert 那行等于不存在
# → average([]) 不再报 AssertionError,而是 sum([])/0 → ZeroDivisionError
# ✗ 错误:用 assert 做安全/输入校验(生产 -O 时失效!)
def withdraw(user, amount):
assert user.is_admin, "无权限" # 危险!-O 时这行没了,越权放行
assert amount > 0, "金额非法" # 危险!-O 时负数放行
# ✓ 正确:用 raise 做校验
def withdraw_ok(user, amount):
if not user.is_admin:
raise PermissionError("无权限")
if amount <= 0:
raise ValueError("金额必须为正")
⚠️ 一句话记住红线:assert 是给「程序员」看的(检查「我的代码有没有 bug」),不是给「用户」用的(检查「用户输入对不对」)——因为
-O会把所有 assert 抹掉,任何「生产环境必须执行」的检查都绝不能依赖它。经典安全事故:assert user.is_authenticated、assert not is_malicious(input)——开发时好好的,一上生产用python -O跑(很多部署为了性能会开),这些断言全部消失,认证/安全检查形同虚设,直接被绕过。正确的心智模型是:assert 检查的是**「不变量(invariant)」——「按我的逻辑,程序运行到这里,这个条件必然为真;如果为假,说明我的代码写错了」(比如内部函数收到了不该收到的值、算法中间状态违反了预期)。而「预期可能为假、需要处理的情况」**(用户传了空、文件不存在、权限不足)是正常的错误处理,要用if...raise显式抛具体异常(ValueError/PermissionError/FileNotFoundError)。
完整版教学
一、assert 是什么
先理解 assert 的语义:
assert(断言):
检查一个"本该为真"的条件,为假就抛 AssertionError
语法:
assert 条件
assert 条件, 错误信息 # 带信息(推荐)
等价展开:
assert cond, msg
≡
if __debug__: # __debug__ 默认 True
if not cond:
raise AssertionError(msg)
执行:
条件为真 → 什么都不做,继续执行
条件为假 → 抛 AssertionError(错误信息)
例子:
assert x > 0 # x 不大于 0 就抛 AssertionError
assert isinstance(y, list), f"y 应是 list,实际 {type(y)}"
assert result == expected # 测试里常见
用途定位:
"我断定这里 cond 必然为真;若不为真,是我的代码 bug"
→ 检查"不该发生的情况",帮助早发现逻辑错误
常见误用(先剧透):
assert 不是"友好的错误提示",也不是"输入校验"
→ 它会在 -O 时消失(见后)
所以 assert 断言:检查本该为真的条件,为假抛 AssertionError,用于捕捉代码 bug
assert(断言) 检查一个「本该为真」的条件、为假就抛 AssertionError。语法:assert 条件 或 assert 条件, 错误信息(带信息推荐)。等价展开:assert cond, msg ≡ if __debug__: if not cond: raise AssertionError(msg)(__debug__ 默认 True)。执行:条件真→什么都不做、条件假→抛 AssertionError。用途定位:「我断定这里 cond 必然为真;若不为真,是我的代码 bug」——检查「不该发生的情况」、帮助早发现逻辑错误。常见误用:assert 不是「友好的错误提示」也不是「输入校验」(它会在 -O 时消失)。理解「assert 检查本该为真的条件为假抛 AssertionError;语法 assert cond,msg;等价 if debug:if not cond:raise;用于捕捉代码 bug 不是输入校验」,就理解了 assert 是什么。
二、-O 会移除所有 assert
理解最关键的坑——优化模式移除断言:
-O(optimize)参数会移除所有 assert:
运行方式:
python -O script.py # 开启优化,移除 assert
python -OO script.py # -O 再加移除文档字符串
设 PYTHONOPTIMIZE=1 环境变量 也等效
效果:
开 -O 时,__debug__ 变成 False
→ 所有 assert 语句被编译器直接跳过(等于不存在)
→ 断言不执行、条件不检查、不会抛 AssertionError
验证:
# test.py
assert False, "should fail"
print("reached")
python test.py → AssertionError: should fail(断言生效)
python -O test.py → reached(断言被移除,直接跑到 print)
为什么有 -O:
生产环境想去掉调试断言的开销(断言检查本身要花时间)
→ -O 一键移除所有断言 + 设 __debug__=False
危险推论:
任何"生产必须执行"的检查放在 assert 里
→ -O 部署时全部失效
→ 这就是"生产不能用 assert 做校验"的根本原因
__debug__ 常量:
正常运行:__debug__ = True(assert 生效)
-O 运行: __debug__ = False(assert 移除)
→ 可用 if __debug__: 写只在调试时执行的代码
所以 -O 移除所有 assert(__debug__=False),生产必须执行的检查不能放 assert
-O(optimize)参数会移除所有 assert。运行方式:python -O script.py(移除 assert)、python -OO(再移除文档字符串)、设 PYTHONOPTIMIZE=1 也等效。效果:开 -O 时 __debug__ 变 False → 所有 assert 语句被编译器直接跳过(等于不存在)、断言不执行不检查。验证:assert False, "should fail" 在 python test.py 抛 AssertionError、在 python -O test.py 直接跳过。为什么有 -O:生产环境想去掉调试断言的开销、一键移除。危险推论:任何「生产必须执行」的检查放 assert 里、-O 部署时全部失效——这是「生产不能用 assert 做校验」的根本原因。__debug__:正常运行 True(assert 生效)、-O 运行 False(assert 移除)。理解「-O 移除所有 assert(__debug__变 False,编译器直接跳过);python -O/PYTHONOPTIMIZE=1;生产必须执行的检查不能放 assert(会失效)」,就掌握了最关键的坑。
三、正确用途——不变量与防御性编程
理解 assert 该用在哪:
assert 的正确用途(检查"不该发生的 bug"):
① 不变量检查(invariant):
"按我的逻辑,这里必然满足某条件,否则是我写错了"
def binary_search(arr, target):
assert arr == sorted(arr), "输入必须已排序" # 前置不变量
...
def process(state):
assert state in ('init','running','done') # 状态合法性
② 中间结果检查:
result = complex_calc()
assert 0 <= result <= 1, f"概率越界: {result}" # 算法应保证
→ 若越界,说明算法逻辑有 bug
③ 后置条件检查:
def sort_impl(data):
out = my_sort(data)
assert len(out) == len(data) # 排序后长度应不变
return out
④ "不可能到达"的分支:
if x > 0: ...
elif x < 0: ...
else:
assert False, "x 不可能既非正又非负" # 防御
⑤ 测试代码(pytest 用 assert):
def test_add():
assert add(2, 3) == 5
共同点:
检查的是"程序员的假设/内部一致性"
为假 = 代码有 bug(不是用户的错、不是外部环境问题)
所以正确用途:不变量/中间结果/后置条件/不可达分支/测试——检查代码 bug
assert 的正确用途(检查「不该发生的 bug」):① 不变量检查(assert arr == sorted(arr) 前置不变量、assert state in (...) 状态合法性);② 中间结果检查(assert 0 <= result <= 1 算法应保证的、越界说明算法有 bug);③ 后置条件检查(assert len(out) == len(data) 排序后长度不变);④ 「不可能到达」的分支(else: assert False, "不可能" 防御);⑤ 测试代码(pytest 用 assert,assert add(2,3)==5)。共同点:检查的是「程序员的假设/内部一致性」、为假 = 代码有 bug(不是用户的错、不是外部环境问题)。理解「正确用途:①不变量②中间结果③后置条件④不可达分支⑤测试;共同点检查程序员假设/内部一致性,为假=代码 bug 不是用户/环境问题」,就掌握了正确用途。
四、错误用途——为什么不能做生产校验
理解 assert 不该用在哪、后果:
错误用途(绝不能用 assert 做):
① 用户输入校验:
✗ assert age > 0, "年龄非法"
→ -O 时不检查,非法数据进入系统
✓ if age <= 0: raise ValueError("年龄非法")
② 权限/认证检查(安全关键):
✗ assert user.is_admin
✗ assert user.is_authenticated
→ -O 时越权放行、认证绕过(严重安全漏洞)
✓ if not user.is_admin: raise PermissionError
③ 关键业务逻辑:
✗ assert balance >= amount, "余额不足"
→ -O 时透支放行
✓ if balance < amount: raise InsufficientFundsError
④ 外部环境检查(可能失败的正常情况):
✗ assert os.path.exists(path)
→ 文件不存在是"正常可能",不是 bug
✓ if not os.path.exists(path): raise FileNotFoundError
后果总结:
这些检查在开发时好好的,一上生产 python -O 部署:
→ 全部消失、检查失效
→ 安全漏洞、数据污染、业务错误直接放行
判断标准(该用 raise 还是 assert):
"这个条件为假是可能发生的正常情况吗?"(用户传错、文件没了、余额不够)
是 → 用 if...raise 具体异常(预期的错误处理)
"这个条件为假意味着我的代码有 bug 吗?"(内部状态违反假设)
是 → 用 assert(捕捉程序员错误)
所以别用 assert 做输入/权限/业务/环境校验(−O 会失效),要用 if...raise
错误用途(绝不能用 assert 做):① 用户输入校验(assert age > 0 -O 时不检查非法数据进入、应 if age<=0: raise ValueError);② 权限/认证检查(安全关键)(assert user.is_admin -O 时越权放行/认证绕过、应 if not user.is_admin: raise PermissionError);③ 关键业务逻辑(assert balance >= amount -O 时透支放行、应 raise);④ 外部环境检查(assert os.path.exists(path) 文件不存在是正常可能不是 bug、应 raise FileNotFoundError)。后果:开发时好好的、一上生产 python -O 部署全部消失、安全漏洞/数据污染/业务错误放行。判断标准:「条件为假是可能发生的正常情况吗?」(用户传错/文件没了/余额不够)是就用 if…raise 具体异常;「条件为假意味着我的代码有 bug 吗?」(内部状态违反假设)是就用 assert。理解「错误用途:输入/权限/业务/环境校验都不能用 assert(−O 失效放行漏洞);判断标准:正常可能失败用 if…raise 具体异常、代码 bug 用 assert」,就掌握了错误用途。
五、assert 的其他细节
理解 assert 的一些易错细节:
细节1:assert (cond, msg) 是个大坑!
assert (x > 0, "错误") # ✗ 括号包成元组
→ 断言的是一个"非空元组",永远为真(元组非空=真值)
→ 永远不会失败(即使 x <= 0)!
正确:assert x > 0, "错误" # 无括号,逗号分隔
IDE/linter 会警告"assertion is always true"
细节2:assert 后不要放有副作用的表达式
✗ assert process_and_check(data) # -O 时 process 也不执行了!
→ 副作用(如修改状态、写日志)在 -O 时消失
→ assert 里只放"纯检查",别放"顺便干活"
细节3:多条件断言
assert a > 0 and b > 0, "a、b 都要为正"
或分开写(信息更明确):
assert a > 0, "a 要为正"
assert b > 0, "b 要为正"
细节4:pytest 的 assert 增强
pytest 会"重写"assert,失败时打印详细的中间值:
assert x == y 失败 → 显示 x、y 各是什么(内省)
→ 测试里放心用 assert(pytest 不受 -O 影响,测试也不该开 -O)
细节5:__debug__ 的用法
if __debug__:
expensive_sanity_check() # 只在非 -O 时执行的调试代码
所以细节:assert(cond,msg)括号成元组永真(大坑)、别放副作用、pytest 增强 assert
assert 的其他细节:细节1:assert (cond, msg) 是大坑——括号包成元组、断言的是一个「非空元组」(永远为真)、永远不会失败(即使 cond 假),正确是 assert cond, msg(无括号、逗号分隔),linter 会警告「assertion is always true」;细节2:assert 后不要放有副作用的表达式(assert process_and_check(data) -O 时 process 也不执行、副作用消失,assert 里只放纯检查);细节3:多条件断言(assert a>0 and b>0 或分开写信息更明确);细节4:pytest 的 assert 增强(pytest 重写 assert、失败时打印详细中间值,测试里放心用、测试不该开 -O);细节5:__debug__ 的用法(if __debug__: expensive_sanity_check() 只在非 -O 时执行的调试代码)。理解「细节:①assert(cond,msg)括号成元组永真(大坑)②别放副作用(-O 时不执行)③多条件分开写更清晰④pytest 增强 assert 打印中间值⑤if __debug__写调试代码」,就掌握了 assert 的细节。
六、总结与实践
总结 assert 的使用:
核心规则:
assert 检查"本该为真的不变量"(捕捉程序员 bug)
-O 运行会移除所有 assert(__debug__=False)
→ 生产必须执行的检查绝不能用 assert
该用 assert(内部一致性):
不变量、前置/后置条件、中间结果、不可达分支、测试
该用 if...raise(预期的错误):
用户输入校验 → ValueError
权限/认证 → PermissionError
业务规则 → 自定义异常
外部资源(文件/网络)→ FileNotFoundError 等
避坑清单:
① 别用 assert 做输入/权限/业务校验(-O 失效)
② 别写 assert (cond, msg)(括号成元组、永真)
③ 别在 assert 里放副作用(-O 时不执行)
④ 断言带清晰的错误信息 assert cond, "为什么"
一句话判断:
条件为假 = "用户/环境的问题(可能发生)" → if...raise
条件为假 = "我的代码 bug(不该发生)" → assert
核心总结:
assert = 给程序员的 bug 检查(-O 会移除)
校验 = 给用户/外部的,用 if...raise
别用 assert 做任何生产必须的检查
所以 assert 检查代码 bug(−O 移除)、校验用 if...raise、避坑元组/副作用
核心规则:assert 检查「本该为真的不变量」(捕捉程序员 bug)、-O 运行移除所有 assert(__debug__=False)、生产必须执行的检查绝不能用 assert。该用 assert(内部一致性):不变量、前置/后置条件、中间结果、不可达分支、测试。该用 if…raise(预期的错误):用户输入校验→ValueError、权限/认证→PermissionError、业务规则→自定义异常、外部资源→FileNotFoundError。避坑清单:① 别用 assert 做输入/权限/业务校验、② 别写 assert (cond, msg)(元组永真)、③ 别在 assert 里放副作用、④ 断言带清晰错误信息。一句话判断:条件为假=用户/环境问题(可能发生)用 if…raise、条件为假=我的代码 bug(不该发生)用 assert。理解「assert 检查代码 bug(−O 移除);内部一致性用 assert、预期错误用 if…raise 具体异常;避坑元组/副作用/带信息;判断用户环境问题 raise、代码 bug assert」,就掌握了总结与实践。
记忆钩子:「assert(断言)检查『本该永远为真的条件』,为假抛 AssertionError,语法 assert cond,msg(等价 if not cond:raise AssertionError(msg));★核心坑:python -O(或 PYTHONOPTIMIZE=1)运行时__debug__变 False、所有 assert 被编译器直接移除(等于不存在),所以任何『生产必须执行』的检查绝不能用 assert(−O 部署时全失效);assert 是给程序员的(检查『我的代码有没有 bug』——不变量/前置后置条件/中间结果/不可达分支/测试),不是给用户的(检查『用户输入对不对』);★错误用法:assert user.is_admin(权限)、assert age>0(输入)、assert balance>=amount(业务)——−O 时越权/非法数据/透支全放行(安全漏洞),正确用 if not cond:raise ValueError/PermissionError;判断标准:条件为假是『可能发生的正常情况』(用户传错/文件没了)用 if…raise 具体异常、是『代码 bug』(内部违反假设)用 assert;两个小坑:assert(cond,msg)括号成元组永真(大坑)、别在 assert 里放副作用(−O 不执行)」。
七、常见误区与追问
- 误区:assert 和 if…raise 效果一样,可以互换。 不能——最大区别是
python -O(优化模式)运行时所有 assert 会被移除、根本不执行,而 if…raise 永远执行;所以 assert 只适合「开发/测试阶段捕捉代码 bug」,任何「生产环境必须执行」的检查(输入校验、权限、业务规则)必须用 if…raise,否则一旦用 -O 部署这些检查就全失效。 - 误区:用 assert 做用户输入校验又简洁又好。 极危险——生产环境很多部署会用
python -O(去掉断言开销),此时assert age > 0这类校验全部消失、非法数据长驱直入;更严重的是assert user.is_admin、assert user.is_authenticated这类安全检查被移除会导致越权/认证绕过;输入校验和安全检查必须用if not cond: raise ValueError/PermissionError(...)。 - 误区:assert (condition, “message”) 这样加括号更清晰。 这是个隐蔽的大坑——
assert (x > 0, "错误")里括号把(x > 0, "错误")变成了一个二元元组,而非空元组永远是真值,所以这个断言永远成立、永远不会失败(即使 x ≤ 0);正确写法是不加括号、用逗号分隔条件和消息:assert x > 0, "错误";linter(pylint/flake8)会对这种写法警告 “assertion is always true”。 - 误区:assert 里可以顺便调用有副作用的函数。 不该——因为 -O 会移除整条 assert 语句,
assert process(data)在 -O 时连 process(data) 都不会执行,它的副作用(修改状态、写日志、初始化)就丢失了;assert 里应该只放「纯粹的条件检查」(无副作用的判断),任何需要执行的操作要放在 assert 之外。 - 追问:assert 加不加错误信息有什么区别,怎么写好?
assert cond失败时只抛AssertionError(没有说明、难定位);assert cond, "说明"会把说明作为 AssertionError 的参数、失败时显示出来,便于快速理解「哪个假设被违反了」;写好的关键是让信息说清「期望什么、实际什么」,比如assert 0 <= p <= 1, f"概率越界: {p}"、assert isinstance(x, list), f"期望 list,实际 {type(x).__name__}";这样断言失败时一眼看出问题;此外 pytest 会重写 assert、失败时自动打印比较双方的值,测试里即使不写信息也有详细输出。 - 追问:
__debug__是什么,和 assert 什么关系?__debug__是 Python 的内建常量:正常运行时为 True、用-O/-OO或设 PYTHONOPTIMIZE 时为 False;assert 语句本质上等价于if __debug__: if not cond: raise AssertionError(...),所以当__debug__为 False(-O 模式)时 assert 整体被跳过(编译器直接不生成断言代码);你也可以直接用if __debug__:包裹「只在开发/调试时执行的昂贵检查或调试代码」,它们同样会在 -O 时被优化掉;注意__debug__是只读的、不能在运行时赋值修改。 - 追问:什么样的检查该用 assert,什么样的该用 raise?给个判断标准。 判断标准是问「这个条件为假,是『可能发生的正常情况』还是『我的代码有 bug』」:如果为假是「预期可能发生、需要正常处理」的(用户传了非法参数、文件不存在、余额不足、网络超时),这是错误处理、要用
if not cond: raise 具体异常(ValueError/FileNotFoundError/PermissionError 等),保证生产环境一定执行;如果为假意味着「按我的逻辑这不该发生、发生了说明代码写错了」(内部状态违反不变量、算法中间结果越界、不可能到达的分支),这是断言、用 assert(帮助开发时早发现 bug、生产可被 -O 优化掉);一句话:assert 面向程序员的假设、raise 面向用户和外部世界的现实。
八、加强记忆
assert(断言)检查「本该永远为真的条件」,为假就抛 AssertionError(语法 assert cond, msg,等价 if not cond: raise AssertionError(msg))。核心坑:python -O(或设 PYTHONOPTIMIZE=1)运行时 __debug__ 变 False、所有 assert 被编译器直接移除(等于不存在)——所以任何「生产必须执行」的检查绝不能用 assert(-O 部署时全失效)。assert 是给程序员的(检查「我的代码有没有 bug」——不变量、前置/后置条件、中间结果、不可达分支、测试),不是给用户的(检查「用户输入对不对」)。错误用法:assert user.is_admin(权限)、assert age > 0(输入)、assert balance >= amount(业务)——-O 时越权/非法数据/透支全放行(安全漏洞),正确用 if not cond: raise ValueError/PermissionError。判断标准:条件为假是「可能发生的正常情况」(用户传错、文件没了、余额不够)就用 if...raise 具体异常;是「代码 bug」(内部违反假设)就用 assert。两个小坑:assert (cond, msg) 括号会包成元组、永远为真(大坑);别在 assert 里放副作用(-O 不执行)。一句话「assert 检查『本该为真的不变量』捕捉代码 bug、−O 会移除所有断言;生产必须的检查(输入/权限/业务)用 if…raise 具体异常、绝不用 assert;判断:正常可能失败用 raise、代码 bug 用 assert;避坑 assert(cond,msg)元组永真、别放副作用」。