Python 系列(四十四) 从零认识 assert 断言,让程序学会检查自己
1 | 作者:李晓辉 |
- Python 系列(一) 从零开始认识 Python,入门其实没那么难
- Python 系列(二) 学 Python 的第一步,把开发环境搭建好
- Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
- Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
- Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
- Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
- Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
- Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
- Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
- Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
- Python 系列(十一) list、tuple、set、dict,到底该怎么选?
- Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
- Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
- Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
- Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
- Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
- Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
- Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
- Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
- Python 系列(二十) 变量作用域是什么?一文搞懂 global 和 nonlocal
- Python 系列(二十一) 搞懂变量、对象和引用,理解函数参数传递机制
- Python 系列(二十二) map() 和 filter() 详解,轻松实现批量数据处理与筛选
- Python 系列(二十三) 嵌套函数与 lambda 表达式详解,搞懂作用域与闭包
- Python 系列(二十四) 搞懂递归函数,从基线条件到实际应用
- Python 系列(二十五) 聊聊模块与 import,让代码告别重复编写
- Python 系列(二十六) Python 模块入门,从 import 到模块复用
- Python 系列(二十七) Python dir() 函数详解,快速查看模块中的函数与属性
- Python 系列(二十八) 不会用 Python 标准库?教你快速查找模块和函数
- Python 系列(二十九) 搞懂 Python sys 模块,让脚本更适合自动化运维
- Python 系列(三十) 为什么 0.1 + 0.2 不等于 0.3?彻底搞懂 Decimal
- Python 系列(三十一) 搞懂 datetime 日期时间,轻松处理时区与时间计算
- Python 系列(三十二) 面向对象编程入门,从零理解类、对象、封装与继承
- Python 系列(三十三) 什么是类和对象?零基础理解 Python class
- Python 系列(三十四) Python 属性详解,搞懂类属性、实例属性和 setter
- Python 系列(三十五) 学会特殊方法,让对象打印出你想要的内容
- Python 系列(三十六) 多个对象如何共享数据?彻底搞懂类变量
- Python 系列(三十七) 搞懂父类、子类和方法重写,真正理解继承与多态
- Python 系列(三十八) 如何判断数据类型?三个内置函数一次讲明白
- Python 系列(三十九) 搞懂异常模型与常见异常类型,读懂程序报错
- Python 系列(四十) 从零学会异常处理,掌握 try、except、else 和 finally
- Python 系列(四十一) ValueError、TypeError 到底是什么?一文搞懂常见异常
- Python 系列(四十二) ValueError、TypeError 到底是什么?一文搞懂常见异常
- Python 系列(四十三) 别只会处理异常!学会自定义异常,让程序更容易维护
- Python 系列(四十四) 从零认识 assert 断言,让程序学会检查自己
- Python 系列(四十五) 使用 sys 模块处理标准输入与输出
- Python 系列(四十六) 使用 open() 读取和写入文件
- Python 系列(四十七) 学会读写文本文件,让程序保存运行结果
- Python 系列(四十八) 搞懂 bytes、bytearray 和二进制读写
- Python 系列(四十九) 使用 seek() 和 tell() 灵活控制文件读取位置
- Python 系列(五十) 掌握 os 和 os.path,实现文件与目录自动化管理
- Python 系列(五十一) 掌握列表推导式,轻松实现列表生成与数据筛选
- Python 系列(五十二) 掌握字典推导式,让字典创建更简单
- Python 系列(五十三) 从零理解生成器,轻松处理大量数据
- Python 系列(五十五) 使用 zip() 同时遍历多个集合
如果我们编写了一个 Linux 服务器巡检脚本,程序计算出磁盘剩余空间为 -20 GB,你会怎么办?
是直接输出这个结果,还是让程序立即告诉我们:这里可能存在问题?
在 Python 中,除了使用
if判断条件是否成立,我们还可以使用assert断言,检查程序内部的一些假设是否符合预期。不过,
assert并不是万能的检查工具。它适合验证开发过程中需要成立的假设,却不能代替正式的输入校验。如果把这两种用途混淆,程序就可能在某些运行环境下出现意想不到的问题。这篇文章将从 Linux 服务器巡检的实际场景出发,带你从零理解
assert的基本语法、执行过程、使用场景,以及它与if和正式输入校验之间的区别。
为什么需要 assert
在前面的文章中,我们已经学习了 Python 的变量、条件判断和异常处理。
如果你还记得 if 的用法,应该知道:我们可以先判断一个条件是否成立,再决定程序接下来应该做什么。
例如,编写一个简单的磁盘巡检程序:
1 | disk_usage = 95 |
运行程序后,会输出:
1 | 警告:磁盘使用率过高 |
这个程序没有问题。因为磁盘使用率达到 95%,已经超过了我们设置的 90% 告警阈值,所以程序输出警告。
但这里有一个值得思考的问题。
假设程序获取到的磁盘使用率不是 95%,而是 150%。
按照上面的代码,程序仍然会输出:
1 | 警告:磁盘使用率过高 |
从程序执行的角度来看,这没有任何问题:150 大于 90,条件成立,因此输出警告。
可是,从实际业务的角度来看,磁盘使用率怎么可能达到 150% 呢?
如果这里的数字代表真实的文件系统使用率,那么我们应该怀疑数据来源、计算逻辑,或者程序内部是否出现了错误。
也就是说,程序虽然能够正常执行,但结果可能已经不符合我们的预期。
这正是我们需要理解 assert 的原因之一。
先理解什么是断言
在日常生活中,我们经常会根据已有信息,认定某件事情应该成立。
例如:
- 一块磁盘的剩余空间不应该是负数。
- 磁盘已用空间与剩余空间之和,应该等于总容量。
- 一个用于计算的函数,应该返回我们约定的数据类型。
- 一个经过特定处理的内部数据结构,应该满足预先定义的规则。
这些条件并不一定需要程序在每次运行时都向用户询问。它们更像是开发者对程序运行过程作出的假设。
如果程序执行到某一步时,这些假设突然不成立了,我们就希望尽早发现问题。
Python 提供的 assert 就可以帮助我们完成这件事。
断言可以理解为:开发者认为某个条件此时应该成立,于是让 Python 检查这个条件。如果条件不成立,就抛出异常,提醒我们程序可能出现了问题。
接下来,我们通过一个最简单的例子来理解它。
从一个最简单的例子开始
先不考虑复杂的服务器巡检场景,我们只检查一个数字是否符合预期。
创建一个文件,命名为 assert_demo.py,输入以下代码:
1 | number = 10 |
如果你使用的是 Linux,可以在终端中执行:
1 | python3 assert_demo.py |
预期输出:
1 | 检查通过,继续执行程序 |
看起来很简单,但我们还是要弄清楚程序到底做了什么。
第一行:
1 | number = 10 |
创建一个名为 number 的变量,并把整数 10 保存到这个变量中。
第二行:
1 | assert number > 0 |
这是本文第一次出现的断言语句。
我们先看它后面的表达式:
1 | number > 0 |
这里的 > 表示大于。
因为 number 的值是 10,所以这个表达式实际上是在判断:
1 | 10 > 0 |
显然,这个条件成立。
在 Python 中,条件判断通常会得到两个结果之一:
True:条件成立。False:条件不成立。
因此,number > 0 的结果是 True,断言检查通过,程序继续执行下一行。
最后:
1 | print("检查通过,继续执行程序") |
输出我们看到的提示信息。
如果把数字改成负数呢
现在,我们只修改第一行:
1 | number = -10 |
再次运行:
1 | python3 assert_demo.py |
程序会抛出类似下面的异常:
1 | Traceback (most recent call last): |
不同 Python 版本显示的行号、代码定位和格式可能略有不同,但关键结果是一样的:程序抛出了 AssertionError。
为什么会这样?
因为现在的判断条件相当于:
1 | -10 > 0 |
这个条件不成立,结果是 False。
当 assert 检查到条件不成立时,Python 就会抛出 AssertionError 异常。由于程序没有捕获这个异常,所以执行流程被中断,后面的 print() 没有机会执行。
注意,这里不是 Python 发现负数以后,自动帮我们修改了数据,也不是 Python 认为所有负数都是错误的。
真正的逻辑是:我们自己写下了 number > 0 这个假设,并要求 Python 检查它。
如果这个条件不符合实际业务要求,那么即使断言成功,程序的设计也可能是错误的。
assert 的基本语法
理解了前面的例子,再来看 assert 的语法就容易多了。
Python 中,断言主要有两种写法。
只检查条件
基本语法如下:
1 | assert condition |
其中,condition 只是用来说明位置的占位名称,并不是 Python 的特殊关键字。
实际编写代码时,我们需要将它替换为真正的判断表达式。
例如:
1 | cpu_usage = 60 |
这里的 >= 表示大于或等于。
由于 cpu_usage 为 60,条件成立,因此程序继续执行。
如果将它修改为:
1 | cpu_usage = -10 |
条件就不成立了,程序会抛出 AssertionError,后面的输出不会执行。
这里需要特别注意:断言不会自动判断 CPU 使用率是否真的合理。我们只是利用 cpu_usage >= 0 检查一个自己定义的条件。
如果还希望检查 CPU 使用率是否超过 100%,可以把条件写得更完整:
1 | cpu_usage = 60 |
这行代码的含义是:
1 | cpu_usage >= 0 and cpu_usage <= 100 |
也就是说,CPU 使用率既不能小于 0,也不能大于 100。
这里使用的是 Python 支持的连续比较写法。对于初学者来说,可以先把它理解为同时检查两个边界。
检查条件并提供错误信息
前面的写法有一个不足:如果断言失败,Python 虽然会告诉我们发生了 AssertionError,但提示信息不一定足够直观。
因此,Python 还支持第二种写法:
1 | assert condition, message |
其中:
condition:需要检查的条件。message:条件不成立时附带的错误信息。
例如:
1 | cpu_usage = -10 |
运行后,异常信息中会包含:
1 | AssertionError: CPU 使用率不能为负数 |
这样,我们就能更快地理解断言为什么失败。
这里有一个细节值得记住:message 并不是条件成立时的输出内容。
只有断言失败时,这个错误信息才会作为异常信息出现。
如果希望用户在输入错误时看到一条友好的提示,并让程序继续等待输入,那么 assert 通常不是合适的选择。后面我们会专门讨论这个问题。
assert 有返回值吗
这是初学者容易产生的疑问。
前面我们说过,条件表达式会得到 True 或 False。那么,assert 是不是也会返回 True 或 False 呢?
不是。assert 是一条语句,而不是一个返回布尔值的函数。
我们需要区分两个概念:
- 条件表达式的结果:例如
number > 0,结果可以是True或False。 assert语句的执行结果:条件成立时,断言通过并继续执行;条件不成立时,抛出AssertionError。
例如:
1 | number = 10 |
运行结果:
1 | True |
这里的 result 保存的是条件表达式 number > 0 的计算结果,而不是 assert 的返回值。
再看一个容易写错的例子:
1 | result = assert number > 0 |
这不是合法的 Python 语法,会导致 SyntaxError。
原因是 assert 是一条独立的语句,不能像函数调用那样放到赋值表达式的右侧。
如果确实需要保存条件的判断结果,可以直接写:
1 | result = number > 0 |
如果还需要在条件不成立时抛出断言异常,则另外写一条断言语句:
1 | result = number > 0 |
这样就清楚地区分了条件判断结果与断言检查。
assert 的语句限制
理解了 assert 没有返回值之后,还需要了解它在 Python 语法中的使用限制。
assert 不能直接作为赋值表达式
下面这种写法是错误的:
1 | result = assert 1 > 0 |
如果运行这段代码,Python 会在语法解析阶段报错。
正确的写法是:
1 | result = 1 > 0 |
其中,result 保存的是布尔值 True,而 assert result 负责检查这个值是否符合预期。
assert 不能直接作为函数参数
例如,下面这种写法同样不合法:
1 | print(assert 1 > 0) |
如果希望输出判断结果,应该写:
1 | print(1 > 0) |
如果希望执行断言检查,则应该单独写:
1 | assert 1 > 0 |
这两种写法的目的不同,不能混为一谈。
assert 不适合执行必须完成的操作
例如:
1 | assert check_disk_status() |
这里的 check_disk_status() 是一个函数调用。如果函数返回 True,断言通过;如果返回 False,断言失败。
但如果启用了 Python 优化模式,整条断言语句会被移除,函数调用也不会执行。
因此,如果函数负责读取磁盘数据、更新状态或执行其他必要操作,就不应该把它放在断言中作为唯一的调用方式。
更合理的写法是:
1 | status_ok = check_disk_status() |
这样,函数调用不依赖于断言,正常运行和优化模式下都会执行。
这里的 check_disk_status() 只是示意函数,实际使用前需要先定义它。
记住:断言用于检查条件,而不是用来承载必须执行的业务操作。
assert 到底是怎样执行的
前面我们分别看到了断言成功和失败的情况。现在,把它们放在一起,就可以理解断言的执行过程。
flowchart TD
A["程序执行到 assert"] --> B["计算断言条件"]
B --> C{"条件是否成立?"}
C -->|是| D["断言通过"]
D --> E["继续执行后续代码"]
C -->|否| F["抛出 AssertionError"]
F --> G{"异常是否被捕获?"}
G -->|是| H["执行对应的异常处理代码"]
G -->|否| I["异常继续向外传播"]我们可以将它理解为程序中的一道检查关卡。
当条件成立时,程序通过关卡,继续向下执行。
当条件不成立时,Python 抛出 AssertionError,交由程序的异常处理机制决定接下来如何处理。
如果没有合适的异常处理代码,异常通常会导致当前执行流程中断,并可能使程序退出。
如果异常被捕获,程序则可以执行相应的处理逻辑。
用代码观察执行顺序
创建或修改 assert_demo.py:
1 | disk_usage = 95 |
先使用 95 运行程序:
1 | python3 assert_demo.py |
预期输出:
1 | 开始检查磁盘使用率 |
因为 disk_usage 的值是 95,而判断条件是:
1 | 95 <= 100 |
这个条件成立,因此程序继续执行最后一条 print()。
现在,我们把第一行修改为:
1 | disk_usage = 150 |
其他代码保持不变,再次运行:
1 | python3 assert_demo.py |
输出的关键部分如下:
1 | 开始检查磁盘使用率 |
这一次,150 <= 100 不成立,Python 抛出 AssertionError,最后一条 print() 不会执行。
通过这个例子,我们可以清楚地看到断言的执行过程:
- 当
disk_usage为95时,条件成立,程序继续执行。 - 当
disk_usage为150时,条件不成立,程序抛出异常。
判断断言是否会失败,必须同时检查代码中的条件和变量的实际值。
在 Linux 服务器巡检中使用 assert
理解了基本语法之后,我们把断言放到一个更接近实际运维工作的场景中。
假设你正在编写一个 Linux 服务器巡检工具,需要统计某个文件系统的总容量、已用空间和剩余空间。
为了便于理解,我们先使用模拟数据,而不是直接执行系统命令。
1 | total_space = 100 |
这里的 total_space 表示总容量,used_space 表示已用空间,单位统一为 GB。
剩余空间的计算方法是:
剩余空间 = 总容量 − 已用空间
对应的 Python 代码:
1 | free_space = total_space - used_space |
代入当前数据:
剩余空间 = 100 − 65 = 35 GB
因此,free_space 的值是 35。
编写完整的巡检示例
创建文件 assert_disk.py,输入以下代码:
1 | # 模拟从服务器获取到的磁盘数据 |
在 Linux 终端中执行:
1 | python3 assert_disk.py |
预期输出:
1 | 磁盘总容量:100 GB |
我们再逐步分析这段代码。
第一个断言:
1 | assert free_space >= 0, "计算结果异常:剩余空间不能为负数" |
它检查剩余空间是否小于零。
如果计算得到的剩余空间是 35,条件成立,程序继续执行。
如果计算结果是 -20,条件不成立,Python 就会抛出 AssertionError。
第二个断言:
1 | assert free_space + used_space == total_space, "计算结果异常:容量之和不等于总容量" |
它检查已用空间和剩余空间相加之后,是否等于总容量。
对应的计算关系是:
已用空间 + 剩余空间 = 总容量
在当前示例中:
65 + 35 = 100
因此,第二个断言也会通过。
故意写错计算公式,看看断言能否发现问题
学习编程时,一个很好的方法就是主动制造错误,观察程序会发生什么。
现在,我们故意把剩余空间的计算公式写错:
1 | total_space = 100 |
这一次,程序计算出的剩余空间为:
100 − 65 − 10 = 25 GB
第一个断言检查的是:
25 ≥ 0
条件成立,因此第一个断言无法发现问题。
第二个断言检查的是:
25 + 65 = 100
这个等式不成立,所以程序会抛出 AssertionError,提示容量之和不等于总容量。
这个例子说明,断言可以帮助我们检查程序内部的计算结果是否满足预先设定的关系。
不过,也要认识到断言的局限性:断言只能检查我们明确写出来的条件,不能自动发现所有逻辑错误。
例如,如果总容量和已用空间本身就来自错误的数据源,那么某些断言仍然可能通过。
在真实的 Linux 巡检程序中,我们还需要考虑挂载点、文件系统预留空间、数据单位以及系统命令的输出格式等问题。
assert 与 if 有什么区别
学到这里,你可能会产生一个疑问:
既然 if 能够检查条件,assert 也能够检查条件,那么为什么不统一使用其中一个?
关键在于:它们虽然都涉及条件判断,但设计目的不同。
if 用于决定程序应该做什么
例如,我们需要根据磁盘使用率输出不同级别的告警:
1 | disk_usage = 95 |
运行结果:
1 | 严重告警:磁盘使用率过高 |
这里使用 if 非常合适。
因为磁盘使用率达到 90% 并不意味着程序本身出现了错误,而是说明业务状态达到了需要告警的程度。
程序应该正常识别这种情况,输出告警信息,并根据业务要求继续执行。
assert 用于验证程序内部的假设
再看一个例子:
1 | disk_usage = 95 |
在这个例子中,95 位于 0 到 100 之间,因此断言通过。
如果 disk_usage 的值是 150,断言就会失败。
这里的断言不是用来判断磁盘是否需要告警,而是检查程序持有的数据是否满足预先设定的范围要求。
可以通过下面的表格区分它们。
| 对比项 | if | assert |
|---|---|---|
| 主要用途 | 业务判断和流程控制 | 验证程序内部的假设 |
| 条件成立时 | 执行对应分支 | 继续执行 |
| 条件不成立时 | 可以执行 else 等分支 | 抛出 AssertionError |
| 是否有返回值 | 条件表达式有判断结果 | 语句本身没有返回值 |
| 适合处理用户输入 | 适合 | 不适合代替正式校验 |
在 python -O 下 | 正常执行 | 断言语句会被移除 |
记住一个简单的原则:如果某种情况是程序正常运行时需要处理的业务状态,优先考虑 if;如果某个条件是开发者认为程序内部理应满足的假设,可以考虑使用 assert。
assert 与正式输入校验的区别
这一部分是整篇文章最重要的内容之一。
因为在实际工作中,我们经常需要处理来自用户、配置文件、API 或其他系统的数据。
这些数据不一定符合我们的预期,因此必须进行可靠的校验。
外部数据为什么不能直接相信
假设我们编写了一个磁盘巡检工具,让用户输入磁盘使用率。
用户可能输入以下内容:
| 用户输入 | 可能代表的情况 |
|---|---|
75 | 合法的使用率 |
95 | 合法的使用率,但可能需要告警 |
-10 | 超出合理范围 |
150 | 超出合理范围 |
abc | 不能直接转换为整数 |
这里需要区分两件事情。
第一,输入的格式是否正确。
例如,abc 不是可以直接转换为整数的字符串。
第二,输入的数值是否符合业务规则。
例如,150 可以成功转换为整数,但对于我们约定的百分比使用率来说,它仍然超出了有效范围。
所以,仅仅能够把输入转换为整数,并不意味着输入一定合法。
为什么不能只用 assert 检查用户输入
创建文件 assert_input.py,输入下面的代码:
1 | disk_usage = int(input("请输入磁盘使用率:")) |
运行程序:
1 | python3 assert_input.py |
当用户输入 75 时,程序可以正常执行。
当用户输入 150 时,断言失败,程序抛出 AssertionError。
乍看之下,这似乎已经完成了输入校验。
但问题在于,Python 支持使用优化模式运行程序。
如果执行:
1 | python3 -O assert_input.py |
那么 assert 语句会被移除。
如果上面的输入校验依赖于断言,150 就可能绕过这项检查,进入后续业务逻辑。
另外,如果用户输入 abc,程序会在执行 int() 时抛出 ValueError。这个错误发生在断言之前,因此即使没有启用优化模式,断言也无法处理它。
这说明,断言既不能替代输入格式校验,也不能作为关键业务规则的唯一保障。
使用 try/except 和 if 进行正式校验
对于外部输入,我们应该明确处理可能发生的错误。
将 assert_input.py 的内容修改为:
1 | try: |
运行程序:
1 | python3 assert_input.py |
如果输入 75,输出如下:
1 | 请输入磁盘使用率:75 |
如果输入 150,输出如下:
1 | 请输入磁盘使用率:150 |
如果输入 abc,输出如下:
1 | 请输入磁盘使用率:abc |
这里涉及我们之前学过的异常处理知识。
try 用于执行可能发生异常的代码:
1 | disk_usage = int(input("请输入磁盘使用率:")) |
如果用户输入的内容无法转换为整数,Python 就会抛出 ValueError。
接下来,except ValueError 捕获这种异常,并输出相应的提示。
如果没有发生转换异常,程序才会进入 else 部分。
然后,if 检查数值是否位于允许的范围内。
这就是正式输入校验的基本思路:先处理数据格式问题,再检查业务规则。
如果是在编写可复用函数或正式服务,还可以在校验失败时抛出 ValueError,让调用方决定如何记录日志、返回 API 错误或者提示用户。
无论采用哪种方式,关键都是:外部数据的校验必须由正常运行时始终执行的代码负责,而不是依赖 assert。
为什么 python -O 会影响 assert
前面多次提到了 python -O。现在,我们单独理解一下这个运行选项。
Python 解释器支持优化模式,其中 -O 是启用优化模式的参数。
正常运行时,Python 会执行断言检查。
启用优化模式后,Python 会移除断言语句。因此,我们不能依赖断言执行必要的业务操作或关键校验。
编写一个测试程序
创建文件 assert_mode.py:
1 | value = -1 |
先正常运行:
1 | python3 assert_mode.py |
因为 value 的值为 -1,条件 value > 0 不成立,所以程序会抛出 AssertionError。
最后一行不会执行。
接下来,启用优化模式:
1 | python3 -O assert_mode.py |
预期输出:
1 | 开始执行程序 |
为什么这一次没有抛出异常?
因为在优化模式下,断言语句被移除了,Python 不再执行这项断言检查。
所以,虽然 value 仍然是 -1,程序却直接执行了后面的 print()。
这也解释了为什么不能把断言当作权限检查、用户输入校验或支付金额校验的唯一保障。
如果一项检查关系到程序的正确性或系统安全,就应该使用正常运行模式下始终执行的校验逻辑。
assert 的常见使用误区
掌握基本语法之后,还需要避免几个容易出错的用法。
误区:把业务规则全部写成断言
例如:
1 | if cpu_usage >= 90: |
这段代码是在处理业务状态。
如果 CPU 使用率超过 90%,程序应该正常触发告警,而不是将其当作程序内部错误。
如果改成:
1 | assert cpu_usage < 90, "CPU 使用率过高" |
那么 CPU 使用率达到 90% 时,程序就会抛出异常。
更重要的是,在优化模式下,这个断言会被移除。
因此,业务规则应该使用合适的条件判断和业务处理逻辑,而不是简单地替换成断言。
误区:认为断言失败后程序一定会退出
断言失败时,Python 会抛出 AssertionError,但这并不意味着程序一定会立即终止。
如果程序使用 try/except 捕获了这个异常,就可以执行相应的处理逻辑。
例如:
1 | try: |
运行结果:
1 | 断言失败:条件不成立 |
这里,1 > 2 不成立,因此断言抛出异常。
随后,except AssertionError as e 捕获异常,并把异常对象保存到变量 e 中。
最后,程序继续执行后面的输出语句。
不过,不要因此就把所有断言失败都当成普通业务情况处理。断言通常用于发现程序内部不符合预期的状态,而不是代替正常的错误处理机制。
误区:在断言表达式中执行必要操作
不建议这样写:
1 | assert check_disk_status() |
如果 check_disk_status() 负责执行必要的磁盘检查,那么在优化模式下,整个断言语句会被移除,函数调用也就不会发生。
更合理的写法是:
1 | status_ok = check_disk_status() |
这里的 check_disk_status() 只是示意函数,需要在实际程序中定义。
关键在于:必要的操作不应该依赖 assert 才会执行。
误区:以为断言能够发现所有错误
例如:
1 | total_space = 100 |
这个断言可以通过。
但假如数据本身来自错误的采集结果,而三个数值恰好仍然满足这个关系,断言就无法发现数据源的问题。
因此,断言并不是自动化的程序审计工具,也不能证明程序完全正确。
它只能检查我们明确写下的条件。
开发者仍然需要结合实际业务逻辑、测试用例、异常处理和数据校验来保证程序质量。
练习:检查磁盘巡检结果
最后,我们通过一个完整练习巩固今天学习的知识。
假设程序已经获得以下数据:
1 | total_space = 200 |
请尝试完成以下任务:
- 计算磁盘剩余空间。
- 使用第一个
assert检查剩余空间不能为负数。 - 使用第二个
assert检查已用空间与剩余空间之和是否等于总容量。 - 在两个断言通过后,输出总容量、已用空间和剩余空间。
建议先自己编写代码,再参考下面的答案。
参考答案
1 | # 定义磁盘总容量和已用空间 |
保存为 assert_practice.py,然后执行:
1 | python3 assert_practice.py |
预期输出:
1 | 磁盘总容量:200 GB |
现在,可以再尝试修改 used_space 的值,或者故意修改剩余空间的计算公式,观察两个断言分别会在什么情况下失败。
通过这种方式,我们不仅能记住 assert 的语法,还能理解它究竟在帮助我们检查什么。
写在最后
通过这篇文章,我们从 Linux 服务器巡检的场景出发,逐步学习了 Python 的 assert 断言。
我们了解了它的两种基本语法,理解了条件表达式与断言语句的区别,观察了断言成功和失败时的执行过程,也学习了如何利用断言验证计算结果和程序内部的假设。
更重要的是,我们区分了断言与正式输入校验的用途。
if:根据条件决定程序应该执行什么业务逻辑。try/except:捕获并处理可能发生的异常。assert:验证开发者认为应该成立的程序内部假设,帮助尽早发现逻辑问题。
记住一个原则:断言是帮助开发者发现问题的工具,不是代替程序处理外部错误的保障。特别是在启用 Python 优化模式时,断言可能被移除,因此不能依赖它执行关键的校验和业务操作。
至此,Python 异常处理相关内容就告一段落了。从异常的捕获与处理,到使用 raise 主动抛出异常、定义自定义异常,再到利用 assert 验证程序假设,我们已经学习了处理程序错误的基础方法。
接下来,我们将进入 Python 的输入与输出数据流,学习如何读取和写入字节序列,为后续处理文件、二进制数据以及其他输入输出场景打下基础。
