1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Python 系列(一) 从零开始认识 Python,入门其实没那么难
  2. Python 系列(二) 学 Python 的第一步,把开发环境搭建好
  3. Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
  4. Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
  5. Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
  6. Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
  7. Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
  8. Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
  9. Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
  10. Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
  11. Python 系列(十一) list、tuple、set、dict,到底该怎么选?
  12. Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
  13. Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
  14. Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
  15. Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
  16. Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
  17. Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
  18. Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
  19. Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
  20. Python 系列(二十) 变量作用域是什么?一文搞懂 global 和 nonlocal
  21. Python 系列(二十一) 搞懂变量、对象和引用,理解函数参数传递机制
  22. Python 系列(二十二) map() 和 filter() 详解,轻松实现批量数据处理与筛选
  23. Python 系列(二十三) 嵌套函数与 lambda 表达式详解,搞懂作用域与闭包
  24. Python 系列(二十四) 搞懂递归函数,从基线条件到实际应用
  25. Python 系列(二十五) 聊聊模块与 import,让代码告别重复编写
  26. Python 系列(二十六) Python 模块入门,从 import 到模块复用
  27. Python 系列(二十七) Python dir() 函数详解,快速查看模块中的函数与属性
  28. Python 系列(二十八) 不会用 Python 标准库?教你快速查找模块和函数
  29. Python 系列(二十九) 搞懂 Python sys 模块,让脚本更适合自动化运维
  30. Python 系列(三十) 为什么 0.1 + 0.2 不等于 0.3?彻底搞懂 Decimal
  31. Python 系列(三十一) 搞懂 datetime 日期时间,轻松处理时区与时间计算
  32. Python 系列(三十二) 面向对象编程入门,从零理解类、对象、封装与继承
  33. Python 系列(三十三) 什么是类和对象?零基础理解 Python class
  34. Python 系列(三十四) Python 属性详解,搞懂类属性、实例属性和 setter
  35. Python 系列(三十五) 学会特殊方法,让对象打印出你想要的内容
  36. Python 系列(三十六) 多个对象如何共享数据?彻底搞懂类变量
  37. Python 系列(三十七) 搞懂父类、子类和方法重写,真正理解继承与多态
  38. Python 系列(三十八) 如何判断数据类型?三个内置函数一次讲明白
  39. Python 系列(三十九) 搞懂异常模型与常见异常类型,读懂程序报错
  40. Python 系列(四十) 从零学会异常处理,掌握 try、except、else 和 finally
  41. Python 系列(四十一) ValueError、TypeError 到底是什么?一文搞懂常见异常
  42. Python 系列(四十二) ValueError、TypeError 到底是什么?一文搞懂常见异常
  43. Python 系列(四十三) 别只会处理异常!学会自定义异常,让程序更容易维护
  44. Python 系列(四十四) 从零认识 assert 断言,让程序学会检查自己
  45. Python 系列(四十五) 使用 sys 模块处理标准输入与输出
  46. Python 系列(四十六) 使用 open() 读取和写入文件
  47. Python 系列(四十七) 学会读写文本文件,让程序保存运行结果
  48. Python 系列(四十八) 搞懂 bytes、bytearray 和二进制读写
  49. Python 系列(四十九) 使用 seek() 和 tell() 灵活控制文件读取位置
  50. Python 系列(五十) 掌握 os 和 os.path,实现文件与目录自动化管理
  51. Python 系列(五十一) 掌握列表推导式,轻松实现列表生成与数据筛选
  52. Python 系列(五十二) 掌握字典推导式,让字典创建更简单
  53. Python 系列(五十三) 从零理解生成器,轻松处理大量数据
  54. Python 系列(五十五) 使用 zip() 同时遍历多个集合

为什么需要自定义异常

前面我们学习了 Python 的异常处理,知道可以使用 try 和 except 捕获程序运行过程中出现的异常,也了解了如何使用 raise 主动抛出异常。

但在实际开发中,Python 内置的异常并不一定能够准确表达业务中出现的问题。

假设我们正在编写一个 Linux 服务器巡检程序,需要检查服务器的运行时间。如果服务器运行时间过短,可能意味着它刚刚发生过重启,需要进一步确认是否存在异常。

例如,我们规定服务器运行时间至少应该达到 24 小时。如果运行时间不足 24 小时,程序就需要生成一条告警信息。

当然,我们可以直接使用 Python 内置的 ValueError 表示运行时间不符合要求。但如果程序中还需要处理磁盘空间不足、服务状态异常等问题,仅仅看到一个通用异常类型,就不容易快速判断具体发生了什么。

能不能创建一个专门表示服务器运行时间异常的异常类型?

当然可以。

Python 允许我们定义自己的异常类型,这就是自定义异常。

创建自定义异常

在 Python 中,自定义异常通常通过继承内置的 Exception 类来实现。

先来看一个最简单的例子:

1
2
class ServerUptimeTooShortError(Exception):
pass

将这段代码保存到 main.py 文件中。

这里的 ServerUptimeTooShortError 表示服务器运行时间不足,是我们自己定义的异常类型。

  • class 是定义类的关键字。
  • ServerUptimeTooShortError 是自定义异常的名称。
  • Exception 是 Python 内置的常规异常基类。继承它后,我们定义的类就可以作为异常使用。
  • pass 表示暂时不需要添加其他代码。

为什么要继承 Exception?

因为我们希望这个异常能够与 Python 原有的异常处理机制配合使用,例如使用 raise 抛出异常,再通过 except 捕获异常。

这样,一个简单的自定义异常就创建完成了。

主动抛出自定义异常

定义异常类型之后,我们还需要编写检查逻辑,决定什么时候抛出异常。

继续修改 main.py,写入下面的完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 定义自定义异常
class ServerUptimeTooShortError(Exception):
pass


# 模拟服务器已经连续运行的小时数
uptime_hours = 12

# 检查服务器运行时间是否达到要求
if uptime_hours < 24:
raise ServerUptimeTooShortError(
f"服务器运行时间不足:当前已运行 {uptime_hours} 小时"
)

print(f"服务器运行正常,已运行 {uptime_hours} 小时")

保存文件后,执行:

1
python3 main.py

程序会输出类似下面的异常信息:

1
2
3
4
Traceback (most recent call last):
File "main.py", line 12, in <module>
raise ServerUptimeTooShortError(
ServerUptimeTooShortError: 服务器运行时间不足:当前已运行 12 小时

实际文件路径和行号可能有所不同。

我们来分析程序的执行过程。

首先,程序将 uptime_hours 设置为 12,表示模拟服务器已经连续运行了 12 小时。

接下来,程序判断:

1
if uptime_hours < 24:

由于 12 小于 24,条件成立,程序执行 raise 语句,抛出 ServerUptimeTooShortError 异常。

因为当前代码没有捕获这个异常,程序就会显示异常追踪信息,最后的 print() 不会执行。

需要注意的是,自定义异常不会自动检查服务器状态。我们仍然需要编写条件判断,并根据检查结果决定是否抛出异常。

另外,这里的运行时间是模拟数据。实际项目中,需要通过系统接口或命令获取真实的运行时间,再进行检查。运行时间不足也不一定意味着服务器发生了故障,是否告警应根据具体业务需求确定。

捕获自定义异常

前面的例子展示了如何抛出自定义异常,但在实际程序中,我们往往不希望遇到错误就直接终止整个程序。

例如,服务器巡检程序可能需要依次检查多台服务器。如果其中一台服务器的检查结果不符合要求,我们希望记录问题并继续检查其他服务器,而不是让整个巡检任务提前结束。

这时,就可以结合之前学习的 try 和 except 来捕获自定义异常。

继续修改 main.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 定义自定义异常
class ServerUptimeTooShortError(Exception):
pass


def check_server_uptime(uptime_hours):
"""检查服务器运行时间是否达到要求。"""

if uptime_hours < 24:
raise ServerUptimeTooShortError(
f"服务器运行时间不足:当前已运行 {uptime_hours} 小时"
)

print(f"服务器运行时间检查通过:{uptime_hours} 小时")


# 模拟检查三台服务器的运行时间
server_uptimes = [48, 12, 72]

for uptime_hours in server_uptimes:
try:
check_server_uptime(uptime_hours)
except ServerUptimeTooShortError as e:
print(f"巡检告警:{e}")

print("所有服务器检查任务已执行完毕")

执行命令:

1
python3 main.py

预期输出:

1
2
3
4
服务器运行时间检查通过:48 小时
巡检告警:服务器运行时间不足:当前已运行 12 小时
服务器运行时间检查通过:72 小时
所有服务器检查任务已执行完毕

这一次,程序没有因为第二台服务器的运行时间不足而直接终止。

我们结合执行过程来看。

  • 第一次循环,uptime_hours 的值是 48。条件 uptime_hours < 24 不成立,函数正常输出检查通过的信息。
  • 第二次循环,uptime_hours 的值是 12。条件成立,函数使用 raise 抛出 ServerUptimeTooShortError 异常。
  • 异常被 except ServerUptimeTooShortError as e 捕获。变量 e 保存异常对象,我们通过 {e} 输出其中的错误信息。
  • 异常被处理后,程序继续执行下一次循环,因此第三台服务器仍然能够正常检查。
  • 循环结束后,程序继续执行最后一条 print() 语句。

这里有一个重要的知识点:异常被捕获并处理后,程序会继续执行异常处理结构之后的代码,而不是返回到抛出异常的位置重新执行。

在这个例子中,异常处理结构位于循环内部,因此捕获异常后,循环能够继续处理下一台服务器。

理解异常处理流程

前面分别学习了自定义异常的定义、抛出和捕获。现在,我们把这些步骤串联起来,看看一个异常从产生到被处理,究竟经历了什么。

flowchart TD
    A["使用 raise 抛出异常"] --> B{"当前代码能否捕获异常?"}
    B -->|能| C["执行匹配的 except 处理逻辑"]
    C --> D["继续执行异常处理结构之后的代码"]
    B -->|不能| E["异常向外层调用代码传播"]
    E --> F{"是否找到其他异常处理代码?"}
    F -->|是| C
    F -->|否| G["输出未处理异常的追踪信息"]
    G --> H["终止当前线程的正常执行"]
    H --> I["主线程中通常导致程序退出"]

这张流程图可以帮助我们理解几个关键点。

异常并不是自动产生的。

在自定义异常的场景中,程序通常先通过 if 判断业务条件,再决定是否执行 raise。如果条件不成立,程序就按照正常流程继续执行。

异常抛出后,会改变正常的执行顺序。

当程序执行 raise 时,会立即离开当前代码的正常执行路径,并尝试寻找能够处理该异常的代码。如果当前函数没有处理它,异常就会向调用它的上层代码传播。

捕获异常后,程序可以继续运行。

如果 except 匹配到了异常,程序就会执行对应的异常处理代码。处理完成后,会继续执行异常处理结构之后的代码,而不是返回到抛出异常的位置继续执行。

没有捕获的异常不一定立即终止整个系统。

如果异常一直没有被捕获,Python 通常会输出异常追踪信息,并终止当前程序。但在 Web 服务、后台任务等复杂程序中,具体影响范围还取决于异常传播到哪里,以及外层框架如何处理它。

理解这套流程后,我们就可以更有针对性地设计程序中的错误处理逻辑。

自定义异常应该怎么设计

初学阶段,使用 pass 创建一个简单的自定义异常通常就足够了。不过,在编写更复杂的程序时,还需要注意几个问题。

优先继承 Exception

对于一般的业务异常,可以直接继承 Exception:

1
2
class ServerUptimeTooShortError(Exception):
pass

不建议为了表示普通业务错误而直接继承 BaseException。BaseException 还包含 KeyboardInterrupt、SystemExit 等用于控制程序退出或中断的异常,通常不应该把它们当作普通业务错误处理。

异常名称应该清楚地表达问题

例如:

1
2
3
4
5
6
7
8
9
10
class ServerUptimeTooShortError(Exception):
pass


class InvalidServerConfigError(Exception):
pass


class DiskSpaceInsufficientError(Exception):
pass

这些名称分别表示服务器运行时间不足、服务器配置无效和磁盘空间不足。

在实际项目中,可以根据业务需要设计异常类型,但不必为每一个简单的判断都创建一个新异常。只有当某类错误需要被单独识别、捕获或处理时,自定义异常才更有价值。

不要把所有异常都混在一起捕获

例如:

1
2
3
4
try:
check_server_uptime(12)
except ServerUptimeTooShortError as e:
print(f"业务检查失败:{e}")

这里明确捕获了 ServerUptimeTooShortError,因此程序能够针对这种错误执行特定的处理逻辑。

如果不加区分地捕获所有异常,可能会把真正的程序缺陷也当成普通业务问题处理,导致故障难以发现。

动手练习

现在,我们把刚才的知识应用到一个简单的磁盘巡检场景中。

假设巡检程序需要检查磁盘使用率。当磁盘使用率超过 90% 时,程序应该抛出一个自定义异常 DiskUsageTooHighError。

请尝试完成下面的任务:

  • 定义 DiskUsageTooHighError 异常类。
  • 编写 check_disk_usage() 函数,接收一个磁盘使用率参数。
  • 当使用率超过 90 时,抛出自定义异常,并提示“磁盘使用率过高”。
  • 使用 try 和 except 捕获这个异常。
  • 分别测试使用率为 75 和 95 时的执行结果。

提示:你可以参考前面 check_server_uptime() 函数的结构,将运行时间判断替换为磁盘使用率判断。

建议先自己编写代码并运行,再对照下面的参考答案检查。

动手练习参考答案

编写完整代码

在当前目录下创建 disk_check.py 文件,将下面的代码写入文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 定义自定义异常,用于表示磁盘使用率过高
class DiskUsageTooHighError(Exception):
pass


def check_disk_usage(usage):
"""检查磁盘使用率是否超过 90%。"""

# 当磁盘使用率超过 90% 时,主动抛出自定义异常
if usage > 90:
raise DiskUsageTooHighError(
f"磁盘使用率过高:当前使用率为 {usage}%"
)

# 如果没有触发异常,说明检查通过
print(f"磁盘使用率正常:{usage}%")


# 分别检查两种情况
usages = [75, 95]

for usage in usages:
try:
check_disk_usage(usage)
except DiskUsageTooHighError as e:
print(f"巡检告警:{e}")

print("磁盘巡检任务已完成")

运行程序

打开终端,进入 disk_check.py 所在的目录,执行:

1
python3 disk_check.py

预期输出:

1
2
3
磁盘使用率正常:75%
巡检告警:磁盘使用率过高:当前使用率为 95%
磁盘巡检任务已完成

分析程序的执行过程

检查第一次数据:75%

程序第一次循环时,变量 usage 的值为 75。

函数执行以下判断:

1
if usage > 90:

因为 75 没有超过 90,条件不成立,因此不会执行 raise 语句,程序继续执行 print(),输出磁盘使用率正常的信息。

检查第二次数据:95%

第二次循环时,变量 usage 的值为 95。

此时,95 > 90 成立,程序执行:

1
2
3
raise DiskUsageTooHighError(
f"磁盘使用率过高:当前使用率为 {usage}%"
)

这条语句创建并抛出一个 DiskUsageTooHighError 异常,同时携带当前磁盘使用率的错误信息。

由于函数调用位于 try 代码块中,异常会被下面的代码捕获:

1
2
except DiskUsageTooHighError as e:
print(f"巡检告警:{e}")

这里的 e 表示捕获到的异常对象。通过 {e},我们可以输出异常携带的错误信息。

异常被捕获后,程序不会因为这次磁盘检查失败而直接终止,而是继续执行下一次循环。

最后输出任务完成信息

两次检查结束后,循环退出,程序继续执行:

1
print("磁盘巡检任务已完成")

因此,即使第二次检查触发了异常,程序仍然能够完成整个巡检流程。

为什么要使用自定义异常

这个例子中,我们也可以直接使用 Python 内置的 ValueError。但使用 DiskUsageTooHighError,能够让程序更加明确地表达磁盘使用率超标这一业务问题。

例如,当程序需要分别处理磁盘空间不足、配置错误和网络连接失败时,就可以针对不同的异常类型编写不同的处理逻辑,而不必将所有问题混在一起。

需要注意的是,这里使用的是模拟数据,并没有真正读取 Linux 服务器的磁盘使用率。在实际运维脚本中,可以先通过系统命令或 Python 库获取磁盘使用率,再调用 check_disk_usage() 进行检查。

另外,当前示例只判断使用率是否超过 90%,没有验证输入是否为有效的数字。实际项目中,如果数据来源不可靠,还应该根据需求增加参数校验。

写在最后

通过本文,我们学习了如何使用 class 和 Exception 定义自定义异常,使用 raise 主动抛出异常,以及使用 try 和 except 捕获自定义异常。

回顾一下这几个关键点:

  • class 用于定义自定义异常类。
  • Exception 是常规自定义异常通常继承的基类。
  • raise 用于主动抛出异常。
  • try 和 except 可以捕获指定的异常,并执行相应的处理逻辑。

到这里,我们已经了解了 Python 异常处理的基本方法,包括捕获内置异常、主动抛出异常,以及创建自己的异常类型。

不过,在某些情况下,我们希望程序在运行时检查一个条件是否成立。例如,我们可能希望确认服务器的 CPU 使用率没有超过预设阈值,或者确认传入函数的参数符合预期。

除了使用 if 编写条件判断,Python 还提供了一个专门用于调试和检查条件的语句:assert。

下一篇,我们就来学习 Python 中的 assert 断言,看看它与普通的 if 判断有什么区别,以及在实际开发中应该如何正确使用。