资讯动态

Python中断言assert语句的基础用法详解

发布时间:2026/10/9 16:06:36 来源:尧图企业网站定制
前言assert的语法小到一眼看完所以很多人学它的时候只记住一句「断言失败就抛AssertionError」然后就把它当成了「简写版的 if 加报错」。这个理解会导致一个非常危险的用法拿assert去校验用户输入、校验权限、校验业务流程。为什么危险因为assert不是运行时永远存在的检查。用python -O启动解释器时所有assert语句会被整体移除一行代码都不生成。官方命令行文档对这个选项的原文是-O Remove assert statements and any code conditional on the value of __debug__.注意「Remove」这个词——不是跳过、不是变成空操作而是编译期直接删掉。也就是说你在本地跑得好好的校验逻辑到了用-O启动的生产环境里会凭空消失而程序不会报任何错。先把结论摆在这里assert只用于「本来就不该发生」的内部一致性检查绝不能用于校验外部输入、权限或业务流程。本文就围绕这一条展开。一、语法与两种形式语言参考里给出的语法是这样的assert_stmt :: assert expression [ , expression ]也就是两种形式。简单形式只给一个表达式# 适用于 Python 3.8assert len(items) 0按语言参考的说法它等价于if __debug__:if not expression:raise AssertionError扩展形式逗号后面再给一个表达式用来说明失败原因# 适用于 Python 3.8assert len(items) 0, items 不应该为空它等价于if __debug__:if not expression1:raise AssertionError(expression2)几个必须记住的细节逗号后面那个表达式是传给AssertionError的实参。从等价形式可以看出它位于「条件不成立」那个分支里所以只有断言失败时才会被求值一次断言通过时它根本不执行。反过来说如果它本身有副作用比如调用了会改状态的函数那些副作用只在失败路径上发生——这让代码行为变得难以预料所以别在断言消息里写有副作用的表达式。第一个表达式被检查的条件则是每次执行到这里都会求值一次然后丢弃。它不是一个免费的东西放在热点循环里是有成本的。抛出的异常类型固定是AssertionError且它继承自Exception。语言参考还提到一点失败表达式的源码不需要你自己写进消息里它会作为栈回溯的一部分显示出来。所以消息应该写「为什么这里不该失败」而不是重复表达式本身。# 适用于 Python 3.8count 3assert count 0, 内部计数不该为负 # ❌ 消息在重复表达式assert count 0, 计数器应在成功递增后保持为正 # ✅ 消息解释了不变量二、__debug__与-O为什么 assert 会被整体移除上面两段等价代码里都出现了if __debug__这是理解assert的关键。__debug__是一个内置变量正常情况下它是True当请求优化命令行选项-O时它是False。而且这个值在解释器启动时就确定了语言参考明确写着「对__debug__赋值是非法的」——你没法在运行时把它改回去。更重要的是它被使用的时机在编译期。语言参考的原文说当编译时请求了优化当前代码生成器不会为assert语句生成任何代码。这解释了为什么-O下的assert不是「求值为 False 然后跳过」而是彻底不存在。相关的两个开关启动方式效果备注正常启动__debug__为Trueassert生效开发与测试的默认状态python -O移除assert及一切依赖__debug__的代码官方原文Remove assert statements...python -OO在-O基础上再丢弃文档字符串适用于极在意体积的场景环境变量PYTHONOPTIMIZE与-O等效从环境变量而非命令行开启优化注意最后一行优化也可能是从环境变量开起来的。这意味着你不能假设「我启动命令里没写-O所以我的断言一定在跑」——部署环境里有没有设这个变量要实际去看。关键的一点是assert被移除时不会给你任何提示。如果它承载的是「必须执行的校验」那就是一次静默的失效——校验没了代码照跑直到某个更晚的时刻以更难看的方式爆出来。三、所以assert 绝不能用来校验外部输入把上一节的结论翻译成一条硬规则凡是「不满足就必须阻止程序继续」的检查都不能写在assert里。下面这些都是明确的反面用法。校验用户输入。用户的输入天然可能不合法这不是「本不该发生」而是「随时会发生」❌assert isinstance(age, int) and age 0, 年龄不合法✅ 显式判断并抛出可被业务层捕获的异常# 适用于 Python 3.8class ValidationError(Exception):输入不合法。def check_age(age):if not isinstance(age, int) or age 0:raise ValidationError(f年龄必须是正整数收到 {age!r})return age校验权限。权限检查是一次安全边界绝不能用会被优化掉的语句承载❌assert user.is_admin, 只有管理员可以执行✅ 显式判断并拒绝# 适用于 Python 3.8class PermissionDeniedError(Exception):没有权限。def delete_user(operator, target_id):if not operator[is_admin]:raise PermissionDeniedError(只有管理员可以删除用户)# ... 执行删除校验业务前置条件。比如「下单前库存必须充足」这是业务规则不是内部不变量❌assert stock quantity, 库存不足✅if stock quantity: raise ConflictError(库存不足)为什么区分得这么细因为这三类检查的失败后果完全不同输入不合法应当返回给用户一个可读的错误权限不足应当记录审计日志并拒绝库存不足应当触发业务分支。而AssertionError的语义是「程序自己有 bug」它不该被当成正常的业务分支来处理也不该被业务层捕获后温和地降级——真出现AssertionError说明代码逻辑本身有问题需要的是修代码。四、assert 该用在哪里把上一节反过来assert的正当用途是表达不变量invariant那些「按代码逻辑推演如果程序正确就不可能不成立」的论断。几个典型场景函数出口的不变量。比如一个「把列表按某个键分组」的函数正常实现下每个分组的元素数加起来应该等于原列表长度。内部数据结构的自洽。比如自己实现一个栈断言「非空时才允许弹出」。算法中的中间状态。比如二分查找中「左边界不大于右边界」。开发期的防御性检查。用断言把「我以为一定成立的假设」写下来一旦假设被打破在开发阶段就立刻暴露。# 适用于 Python 3.8def normalize_scores(scores):把分数归一到 0 到 1 之间。这里断言的是内部不变量调用方保证传入的是同一长度的两个列表。assert len(scores) 0, 内部不变量归一化前不应传入空序列low, high min(scores), max(scores)assert high low, 内部不变量最大值不应小于最小值if high low:return [0.5 for _ in scores]return [(s - low) / (high - low) for s in scores]注意这段代码里断言的位置和措辞它们检查的都是「按上面的代码推演不可能不成立」的事而且消息里都写了「内部不变量」四个字——这正是assert该待在的地方。还有一类间接受益者测试代码。测试框架在收集和执行测试时会自行处理断言成熟的框架通常会做断言重写让失败信息比原生AssertionError丰富得多。不过要注意由于这一层改写来自框架如果用-O跑测试原生assert依然会被移除——测试应当按框架文档给出的方式运行不要用优化选项。具体行为以你所用的测试框架官方文档为准。一条实用的判断口诀问题用 assert这是外部输入吗不用显式校验并抛业务异常这是权限或安全边界吗不用显式判断并拒绝这是业务规则吗不用走正常业务分支这是「我推演过不可能发生」的内部假设吗用这正是它的用武之地常见坑点1. 用assert校验用户输入❌assert age 0, 年龄不合法。用户传个-1正常模式下程序崩了-O模式下校验干脆消失。 ✅ 显式if判断抛一个业务异常如ValidationError让调用方决定怎么反馈给用户。2. 用assert做权限检查❌assert user.is_admin。-O启动后这道门直接不存在。 ✅ 显式判断并抛出权限异常权限是安全边界不能依赖会被删掉的语句。3. 以为「消息表达式不会执行」❌assert cond, f重试次数 {retry()}以为只有失败时才调用retry()。 ✅ 消息是构造AssertionError的实参条件成立时不构造它、不执行但条件不成立时会执行——所以别在消息里写有副作用或昂贵的表达式。4. 在热点路径里堆断言❌ 在百万次循环的内部逐次断言把开销算进了正常运行。 ✅ 断言本身要付出一次表达式求值的代价把它放在初始化、入口或出口这类低频位置。5. 把AssertionError当成业务异常来捕获❌ 在外层写except AssertionError:然后降级处理把内部 bug 伪装成正常流程。 ✅ 出现AssertionError说明代码有逻辑缺陷应当让它冒出来并在开发阶段修掉而不是掩盖。6. 假设「没写-O就一定有断言」❌ 只看启动命令忽略环境变量PYTHONOPTIMIZE也可能开启优化。 ✅ 部署前确认运行时环境别把关键校验押在断言上这也是前几条规则的根本原因。7. 把断言当文档注释❌ 写一堆assert isinstance(x, int)来「说明」参数类型页面上看着很整齐。 ✅ 类型说明用类型注解如def f(x: int) - int:运行期校验用显式ifassert只留给真正的不变量。8. 用括号包住断言的两个表达式❌assert (cond, 消息)想写成「像函数调用」的样子。 ✅ 那不是扩展形式——括号把两样东西变成了一个元组而元组恒为真值非空断言永远通过。正确写法是assert cond, 消息。总结事项结论语法assert 表达式或assert 表达式, 消息等价形式if __debug__: if not 表达式: raise AssertionError__debug__启动时确定正常为True-O下为False赋值非法-O的后果官方原文Remove assert statements——编译期整条删掉该用在哪内部不变量、「本不该发生」的一致性检查不该用在哪外部输入校验、权限校验、业务规则收尾断言失败意味着代码有 bug修代码而不是捕获它assert的正确心态是它是写给「未来读代码的人」和「开发期的自己」看的用来把隐含的假设显式写下来。一旦某个检查关系到用户数据、权限或业务流程它就必须是一行实实在在的if——因为只有这样的代码才不会在某次优化启动后悄悄消失。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑