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 到底是什么?一文搞懂常见异常

前面我们学习了 Python 的类和对象,也认识了 type()、isinstance() 和 issubclass() 等内置函数。

到目前为止,我们已经能够编写一些简单的程序,让它们按照预期完成任务。

但是,在实际工作中,程序并不总是能顺利运行。

比如,我们编写了一个 Python 脚本,用来统计服务器的 CPU 使用率。程序要求输入一个数字,结果有人输入了 abc;又或者,我们需要读取一个日志文件,但文件实际上并不存在。

这些情况都可能导致程序报错。如果程序一遇到错误就直接退出,那么自动化运维脚本还能可靠地工作吗?

为了让程序能够更好地应对意外情况,我们需要先理解 Python 的异常模型,弄清楚异常是如何产生的、不同异常之间有什么关系,以及异常发生后程序究竟会发生什么。

从一个实际问题开始

假设我们正在编写一个简单的服务器监控程序,需要用户输入 CPU 使用率,然后判断服务器是否需要告警。

先创建一个名为 cpu_check.py 的文件,写入下面的代码:

1
2
3
4
5
6
7
8
cpu_usage = input("请输入服务器的 CPU 使用率:")

cpu_usage = float(cpu_usage)

if cpu_usage >= 80:
print("CPU 使用率过高,需要检查服务器!")
else:
print("CPU 使用率正常。")

这段代码暂时不用急着完全理解。我们先关注它的运行过程。

打开终端,进入保存 cpu_check.py 的目录,然后执行:

1
python cpu_check.py

如果输入 65,程序会输出:

1
2
请输入服务器的 CPU 使用率:65
CPU 使用率正常。

如果输入 90,程序会输出:

1
2
请输入服务器的 CPU 使用率:90
CPU 使用率过高,需要检查服务器!

看起来一切正常。

但是,如果有人输入 abc 呢?

1
2
3
4
5
请输入服务器的 CPU 使用率:abc
Traceback (most recent call last):
File "cpu_check.py", line 3, in <module>
cpu_usage = float(cpu_usage)
ValueError: could not convert string to float: 'abc'

程序报错了,而且后面的 if 判断也没有执行。

问题出在这一行:

1
cpu_usage = float(cpu_usage)

前面我们学过,input() 接收用户输入的内容,并以字符串的形式返回。

因此,即使用户输入的是 65,input() 返回的也不是整数或浮点数,而是字符串 "65"。

float() 可以把合法的数字字符串转换成浮点数:

1
2
print(float("65"))
print(float("90"))

输出:

1
2
65.0
90.0

但是,"abc" 不是合法的数字字符串,Python 无法完成转换,于是引发了 ValueError 异常。

为什么 Python 会产生这种错误?ValueError 又是什么?为什么它会导致后续代码不再执行?

要回答这些问题,我们先从异常的基本概念开始。

什么是异常

异常(Exception)是 Python 在执行程序时,用来表示错误或其他异常情况的一种机制。

当 Python 执行某条语句时,如果发现当前操作无法正常完成,就可能引发异常。

例如,执行下面的代码:

1
print(10 / 0)

除法运算的除数不能为零,因此 Python 会引发 ZeroDivisionError 异常。

再看一个例子:

1
number = int("hello")

int() 可以把合法的整数形式字符串转换成整数,但 "hello" 不是合法的整数表示,因此 Python 会引发 ValueError 异常。

还有一个例子:

1
2
with open("server.log", "r", encoding="utf-8") as file:
content = file.read()

这段代码尝试读取当前目录下的 server.log 文件。如果文件不存在,Python 通常会引发 FileNotFoundError 异常。

这几个例子发生的原因不同:

  • 除数为零,属于 ZeroDivisionError。
  • 数值转换失败,属于 ValueError。
  • 文件不存在,属于 FileNotFoundError。

它们都属于 Python 的异常体系。

异常的意义,不只是告诉我们程序出错了,还可以让程序识别问题的类型,并决定如何应对。

不过,异常并不一定意味着程序员写错了代码。用户输入了不符合要求的数据、文件暂时不存在、网络请求失败,都可能是程序需要考虑的意外情况。

语法错误与异常有什么区别

Python 初学者经常把所有报错都叫作异常,但从 Python 的概念来看,语法错误和运行过程中引发的异常需要区分开来。

语法错误:代码不符合规则

先看下面的代码:

1
2
if 80 > 60
print("CPU 使用率需要关注")

第一行的条件判断语句末尾缺少了冒号 :。

在 Python 中,if 语句需要使用冒号引出代码块。缺少冒号,就不符合 Python 的语法规则。

执行这个文件时,Python 会报告 SyntaxError。

语法错误意味着代码无法按照 Python 的语法规则被正确解析。对于普通脚本,Python 通常会在执行相关代码前报告这类错误,因此程序无法按预期运行。

修复方式通常是检查拼写、括号、冒号和缩进等语法问题。

异常:程序执行过程中出现问题

再看下面的代码:

1
2
cpu_usage = 80
print(cpu_usage / 0)

这段代码的语法没有问题。

Python 可以理解赋值语句,也可以理解除法表达式。

但是,程序执行到 cpu_usage / 0 时,发现除数为零,于是引发 ZeroDivisionError 异常。

两者的区别可以概括为:

对比项语法错误运行时异常
发生原因代码不符合 Python 语法规则执行某个操作时发生错误或异常情况
常见例子SyntaxErrorValueError、ZeroDivisionError
程序状态相关代码无法被正确解析程序可能已经执行了前面的语句
处理思路修改代码语法分析异常原因,修复问题或处理异常

需要注意,SyntaxError 本身也是 Python 异常体系中的一个异常类型。这里的区别主要是从错误发生的阶段和排查方式来理解,而不是说语法错误完全不属于异常体系。

Python 的异常继承关系

前面我们认识了 ValueError、TypeError 和 FileNotFoundError 等异常类型。

那么,Python 为什么要定义这么多异常?它们之间又有什么关系?

这就涉及面向对象编程中的一个概念:继承。

还记得之前学过的类和继承吗?一个子类可以继承父类的属性和方法。Python 的异常类型也采用了类似的继承机制。

例如,ValueError 是 Exception 的子类,而 Exception 又是 BaseException 的子类。

这意味着,异常类型之间并不是彼此独立的,它们组成了一棵继承树。

异常继承关系图

下面这张图展示了 Python 中常见的内置异常类型及其继承关系。为了让图更容易阅读,我们将常见异常按类别分组,并保留重要的中间父类。

flowchart TB
    A["BaseException"]

    A --> B["Exception"]
    A --> C["KeyboardInterrupt"]
    A --> D["SystemExit"]
    A --> E["GeneratorExit"]

    B --> F["ArithmeticError"]
    B --> G["LookupError"]
    B --> H["OSError"]
    B --> I["ValueError"]
    B --> J["TypeError"]
    B --> K["NameError"]
    B --> L["SyntaxError"]
    B --> M["RuntimeError"]
    B --> N["ImportError"]
    B --> O["AssertionError"]

    F --> F1["ZeroDivisionError"]
    F --> F2["OverflowError"]
    F --> F3["FloatingPointError"]

    G --> G1["IndexError"]
    G --> G2["KeyError"]

    H --> H1["FileNotFoundError"]
    H --> H2["PermissionError"]
    H --> H3["TimeoutError"]

    K --> K1["UnboundLocalError"]

    L --> L1["IndentationError"]

    M --> M1["RecursionError"]

    N --> N1["ModuleNotFoundError"]

    classDef root fill:#e8eaf6,stroke:#3949ab,color:#1a237e
    classDef common fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
    classDef special fill:#fff3e0,stroke:#ef6c00,color:#e65100

    class A root
    class B common
    class C,D,E special

阅读这张图时,先记住最重要的两个层级:

  • BaseException 是内置异常体系的根基类。
  • Exception 是大多数常见程序错误的基类。

KeyboardInterrupt、SystemExit 和 GeneratorExit 是 BaseException 的直接子类,而不是 Exception 的子类。

图中每一条连线都表示真实的继承关系。例如:

  • ZeroDivisionError 继承自 ArithmeticError,而 ArithmeticError 继承自 Exception。
  • FileNotFoundError 继承自 OSError,而 OSError 继承自 Exception。
  • UnboundLocalError 继承自 NameError。
  • IndentationError 继承自 SyntaxError。
  • ModuleNotFoundError 继承自 ImportError。

这是一张用于入门学习的简化图,并不是 Python 所有内置异常的完整清单。

为什么需要异常继承关系

假设我们需要读取一个文件,代码可能引发 FileNotFoundError。如果我们只关心文件不存在的问题,就可以针对这个异常类型进行处理。

但是,如果我们希望处理多种常见的程序运行错误,也可以选择它们共同的父类 Exception。

例如,ValueError、TypeError 和 FileNotFoundError 都是 Exception 的后代类型。

因此,在后续学习异常处理时,我们可以通过 except Exception 捕获大部分常见的程序异常。

这里先记住一个原则:捕获父类异常,可以覆盖它的许多子类异常。

不过,Exception 并不是 BaseException 的所有子类的父类。KeyboardInterrupt 和 SystemExit 就不属于 Exception 的子类,所以通常不会被 except Exception 捕获。

这是 Python 异常继承设计中一个很重要的区别。

用代码验证继承关系

前面我们学习过 issubclass(),它可以判断一个类是否是另一个类的子类。

现在用它验证异常继承关系。

创建一个名为 exception_types.py 的文件:

1
2
3
4
5
6
7
print(issubclass(ValueError, Exception))
print(issubclass(TypeError, Exception))
print(issubclass(FileNotFoundError, OSError))
print(issubclass(Exception, BaseException))

print(issubclass(KeyboardInterrupt, Exception))
print(issubclass(KeyboardInterrupt, BaseException))

运行程序:

1
python exception_types.py

输出结果:

1
2
3
4
5
6
True
True
True
True
False
True

逐行理解:

  • ValueError 是 Exception 的子类,因此结果是 True。
  • TypeError 是 Exception 的子类,因此结果是 True。
  • FileNotFoundError 是 OSError 的子类,因此结果是 True。
  • Exception 是 BaseException 的子类,因此结果是 True。
  • KeyboardInterrupt 不是 Exception 的子类,因此结果是 False。
  • KeyboardInterrupt 是 BaseException 的子类,因此结果是 True。

这和前面的继承关系图是一致的。

为什么不建议随便捕获所有异常

既然所有内置异常都可以追溯到 BaseException,为什么不直接捕获 BaseException 呢?

因为 BaseException 的范围太大。

例如,用户按下 Ctrl+C,可能引发 KeyboardInterrupt;程序调用退出机制时,也可能产生 SystemExit。这些异常往往表达的是中断或退出意图,而不只是普通的业务错误。

通常,我们应该优先捕获自己能够合理处理的具体异常,或者在确有需要时捕获 Exception。

不建议为了让程序“看起来不会报错”,就捕获所有异常并忽略它们。否则,程序可能把真正重要的问题隐藏起来。

常见异常类型现场练习

认识异常名称还不够。在实际编写程序时,我们更需要根据代码和错误现象,判断 Python 可能引发哪一种异常。

下面结合一些简单的 Linux 运维场景进行练习。

练习方法: 先阅读代码,猜测会发生什么,再运行代码验证。每个练习都可以单独复制到 Python 文件中执行。

数字转换失败:ValueError

运维脚本经常需要读取配置文件或用户输入,再将内容转换成数字。

例如:

1
2
cpu_usage = float("high")
print(cpu_usage)

思考一下:"high" 能转换成浮点数吗?

运行后会看到类似的错误:

1
ValueError: could not convert string to float: 'high'

这里引发的是 ValueError。

原因是传入的对象类型没有问题:float() 本来就可以接收字符串。但字符串的内容不是合法的数字表示,因此转换失败。

记忆方法: 值不符合当前操作的要求,优先考虑 ValueError。

对象类型不合适:TypeError

再看一个场景:

1
2
3
server_port = 8080
message = "服务器端口:" + server_port
print(message)

我们希望把端口号拼接到提示文字后面,但 server_port 是整数,而字符串拼接要求参与拼接的内容是字符串。

运行后会看到类似的错误:

1
TypeError: can only concatenate str (not "int") to str

这里引发的是 TypeError。

可以通过显式转换解决这个问题:

1
2
3
server_port = 8080
message = "服务器端口:" + str(server_port)
print(message)

输出:

1
服务器端口:8080

记忆方法: 操作本身不适用于当前对象类型,优先考虑 TypeError。

除数为零:ZeroDivisionError

监控程序可能需要计算磁盘使用率。如果计算过程中出现除数为零,就会引发异常。

1
2
3
4
5
used_space = 80
total_space = 0

usage = used_space / total_space
print(usage)

运行后会看到类似的错误:

1
ZeroDivisionError: division by zero

这里引发的是 ZeroDivisionError,它属于 ArithmeticError 的子类。

实际开发中,如果 total_space 表示磁盘总容量,那么它为零可能意味着配置或数据有问题。除了处理异常,还应该检查数据来源是否合理。

字典中不存在指定的键:KeyError

假设我们使用字典保存服务器信息:

1
2
3
4
5
6
server = {
"name": "web01",
"ip": "192.168.1.10"
}

print(server["cpu"])

字典中只有 name 和 ip 两个键,没有 cpu。

通过 server["cpu"] 访问不存在的键时,Python 会引发 KeyError。

这是因为字典的方括号访问方式要求指定的键存在。

如果只是希望在键不存在时返回一个默认值,可以考虑使用字典的 get() 方法:

1
2
3
4
5
6
server = {
"name": "web01",
"ip": "192.168.1.10"
}

print(server.get("cpu", "未知"))

输出:

1
未知

这里没有引发异常,因为 get() 在找不到指定键时会返回我们提供的默认值。

列表索引越界:IndexError

假设我们使用列表保存服务器名称:

1
2
3
servers = ["web01", "web02", "db01"]

print(servers[5])

列表只有三个元素,索引分别是 0、1 和 2。访问索引 5 时,超出了列表范围,因此会引发 IndexError。

注意,Python 的列表索引从 0 开始,而不是从 1 开始。

文件不存在:FileNotFoundError

运维脚本经常需要读取日志或配置文件。

1
2
3
4
with open("missing.log", "r", encoding="utf-8") as file:
content = file.read()

print(content)

如果当前目录下不存在 missing.log,Python 通常会引发 FileNotFoundError。

它属于 OSError 的子类。

遇到这种问题时,除了检查异常类型,还需要检查文件路径、文件名以及运行脚本时所在的目录。

使用了未定义的变量:NameError

再看下面的代码:

1
print(server_name)

如果程序之前没有定义 server_name,Python 就无法找到这个名称对应的变量或其他可用对象,因此会引发 NameError。

需要注意,NameError 与 TypeError 不同。前者是名称无法解析,后者是操作与对象类型不匹配。

练习结果对照

代码场景常见异常类型判断依据
float("high")ValueError字符串内容无法转换成浮点数
"端口:" + 8080TypeError字符串不能直接与整数拼接
80 / 0ZeroDivisionError除数为零
server["cpu"],但键不存在KeyError字典中找不到指定键
servers[5],但列表长度不足IndexError列表索引越界
打开不存在的文件FileNotFoundError文件无法找到
使用未定义的 server_nameNameError名称没有定义

这些是常见情况,并不意味着只要出现相似的表面现象就一定是对应异常。实际异常类型还取决于具体代码和执行环境。

Python 报错后,程序究竟发生了什么

现在我们知道了异常的基本概念和继承关系,接下来需要理解另一个容易混淆的问题:

异常发生、程序执行终止和操作系统进程退出,是同一件事吗?

答案是:不是。

异常发生,不代表进程立刻退出

先看一段简单的代码:

1
2
3
4
5
print("开始执行")

print(10 / 0)

print("继续执行")

运行后,输出大致如下:

1
2
3
4
5
开始执行
Traceback (most recent call last):
File "exception_demo.py", line 3, in <module>
print(10 / 0)
ZeroDivisionError: division by zero

注意,输出中没有出现“继续执行”。

为什么?

因为程序执行到 10 / 0 时,引发了 ZeroDivisionError。如果没有相应的异常处理逻辑,Python 会中断当前正常的执行流程,寻找能够处理这个异常的代码。

这个寻找处理代码的过程,可以简单理解为:Python 会沿着当前的函数调用关系向外查找,看看有没有合适的异常处理代码。

如果最终找不到能够处理该异常的代码,异常就成为未处理异常。

对于普通 Python 脚本,如果未处理异常发生在主线程中,Python 通常会打印错误追踪信息,并结束主线程的执行。

但这里需要注意:主线程结束,不一定意味着整个操作系统进程在同一时刻退出。

如果进程中还有其他非守护线程在运行,Python 进程仍然可能继续存活,直到这些线程也结束,或者进程通过其他方式退出。

因此,我们不能简单地把“发生异常”理解为“操作系统进程立即退出”。

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

再看下面的代码:

1
2
3
4
5
6
7
8
print("开始执行")

try:
print(10 / 0)
except ZeroDivisionError:
print("计算失败:除数不能为零")

print("继续执行")

这里先不要求你完全理解 try 和 except 的语法,只需要观察程序执行结果。

输出如下:

1
2
3
开始执行
计算失败:除数不能为零
继续执行

这一次,虽然 10 / 0 仍然引发了 ZeroDivisionError,但是程序中存在可以处理该异常的代码。

Python 执行了异常处理逻辑,处理完成后,又继续执行了后面的 print("继续执行")。

因此,异常并不一定会导致整个程序终止。

是否继续执行,取决于异常有没有被适当地处理,以及处理完成后的程序流程。

异常终止与进程退出的区别

我们把这几个概念放在一起比较。

概念含义例子
异常发生某个操作引发异常10 / 0 引发 ZeroDivisionError
当前执行流程中断Python 不再按照原来的顺序执行出错位置后面的语句,转而寻找异常处理逻辑未处理异常导致当前调用链退出
线程执行终止某个线程结束执行主线程因未处理异常而结束
操作系统进程退出进程生命周期结束,不再运行Python 解释器进程退出,操作系统回收相应资源

在日常使用的简单 Python 脚本中,如果主线程遇到未处理异常,并且没有其他线程或特殊机制影响进程生命周期,我们通常会观察到:Python 打印异常信息,然后命令执行结束。

但这是常见情形,不是说每次异常都会立即结束整个进程。

如何查看 Python 进程的退出状态

在 Linux 运维中,我们不仅需要观察程序是否报错,还经常需要判断脚本是否执行成功。

操作系统提供了进程退出状态这一机制。对于通过 shell 执行的普通命令,可以使用 $? 查看上一条命令的退出状态。

先创建 exit_status.py:

1
2
3
print("开始执行")
print(10 / 0)
print("继续执行")

执行下面的命令:

1
2
python exit_status.py
echo $?

在常见的 Linux 环境中,输出会包含 ZeroDivisionError 的错误信息,随后显示一个非零退出状态,通常为 1。

例如:

1
2
3
4
5
6
开始执行
Traceback (most recent call last):
File "exit_status.py", line 2, in <module>
print(10 / 0)
ZeroDivisionError: division by zero
1

这里的 1 表示 Python 进程以非零状态退出,通常意味着执行失败。

但要注意,非零退出状态不是异常对象本身,也不等于异常类型。 它是程序向操作系统报告执行结果的一种方式。

不同程序可以使用不同的非零状态表示失败原因。Python 普通脚本因未处理异常退出时,常见的状态是 1;如果程序显式使用 sys.exit(),也可以指定其他退出状态。

程序主动退出,也不一定是异常故障

Python 还可以通过退出机制主动结束程序。

例如:

1
2
3
4
5
6
7
import sys

print("开始执行")

sys.exit(0)

print("继续执行")

运行后,程序会打印:

1
开始执行

后面的 print("继续执行") 不会执行。

这里的 sys.exit(0) 会引发 SystemExit,请求 Python 解释器退出。对于普通脚本,这通常会结束整个 Python 进程,并报告退出状态 0,表示正常结束。

这与前面的 ZeroDivisionError 不同:

  • ZeroDivisionError 通常表示某个操作无法完成。
  • SystemExit 用于请求退出 Python 解释器,可以携带退出状态。
  • 退出状态 0 通常表示成功,非零状态通常表示失败或其他非正常结果。

SystemExit 属于 BaseException 的直接子类,而不是 Exception 的子类。因此,通常使用的 except Exception 不会捕获它。

这个例子说明,程序结束不一定是因为发生了普通运行错误。程序也可能按照设计主动退出。

异常模型的执行过程

现在,我们可以把前面学到的知识串起来。

当 Python 执行一段代码时,可能出现三种不同情况:

  • 代码存在语法错误,无法按照预期解析和执行。
  • 代码正常执行,没有发生异常。
  • 代码执行过程中引发异常,Python 寻找相应的处理逻辑。

如果异常得到了处理,程序可以按照处理逻辑继续执行;如果异常没有得到处理,程序通常会结束当前线程的执行,并在普通脚本场景下显示错误信息。

flowchart TD
    A["开始运行 Python 程序"] --> B{"代码是否存在语法错误?"}

    B -->|是| C["报告 SyntaxError 等语法问题"]
    C --> D["修复代码后重新运行"]

    B -->|否| E["执行程序语句"]
    E --> F{"是否引发异常?"}

    F -->|否| G["继续执行后续语句"]
    G --> H["程序按正常流程结束"]

    F -->|是| I["根据异常类型寻找处理逻辑"]
    I --> J{"是否有匹配的处理逻辑?"}

    J -->|是| K["执行异常处理代码"]
    K --> L{"处理后是否继续执行?"}
    L -->|是| G
    L -->|否| M["结束当前执行流程"]

    J -->|否| N["异常未被处理"]
    N --> O["报告异常信息"]
    O --> P["当前线程执行终止"]
    P --> Q{"进程是否还有其他活动线程或退出条件?"}
    Q -->|进程仍需运行| R["进程暂时继续存活"]
    Q -->|没有其他运行条件| S["Python 进程退出"]

这张图需要特别注意两点。

第一,异常处理不是让错误凭空消失,而是让程序在异常发生后执行预先设计的逻辑。

第二,线程执行终止和进程退出之间还可能存在其他因素。对于简单脚本,两者通常表现得很接近;对于包含多线程、后台任务或其他运行机制的程序,就需要区分它们。

动手验证:观察异常如何影响程序

为了加深理解,我们再做一个简单实验。

创建一个名为 exception_flow.py 的文件:

1
2
3
4
5
6
7
8
9
10
print("步骤一:程序开始")

try:
print("步骤二:准备计算")
result = 10 / 0
print("步骤三:计算完成")
except ZeroDivisionError:
print("步骤四:捕获到除零异常")

print("步骤五:程序继续执行")

执行程序:

1
python exception_flow.py

输出结果:

1
2
3
4
步骤一:程序开始
步骤二:准备计算
步骤四:捕获到除零异常
步骤五:程序继续执行

为什么没有出现“步骤三:计算完成”?

因为 10 / 0 引发了异常,程序立即离开 try 代码块,转到匹配的 except ZeroDivisionError 代码块执行。

异常处理完成后,程序继续执行后面的语句。

这个实验展示了一个非常重要的执行规律:

发生异常后,出错语句后面尚未执行的代码不会自动继续执行;Python 会先寻找并执行匹配的异常处理逻辑。

如果没有匹配的处理逻辑,异常就可能继续向外传播,直到找到能够处理它的代码;如果始终找不到,异常就会成为未处理异常。

这也是为什么我们要学习 try 和 except。

写在最后

今天,我们从一个简单的 CPU 监控脚本出发,认识了 Python 的异常模型。

  • 语法错误与运行时异常:语法错误表示代码不符合语法规则;运行时异常通常是在执行操作时发生的错误或异常情况。
  • 异常继承关系:BaseException 是内置异常体系的根基类,Exception 是大多数常见程序错误的基类。ValueError、TypeError、FileNotFoundError 等都属于 Exception 的后代类型。
  • 异常处理机制:异常发生后,Python 会寻找匹配的处理逻辑。异常得到处理后,程序可以继续执行。
  • 异常终止与进程退出:未处理异常通常会中断当前线程的正常执行,但不一定意味着整个进程立即退出。对于普通脚本,主线程的未处理异常通常会导致脚本以非零退出状态结束。
  • 常见异常类型:通过数字转换、字典查询、列表索引和文件读取等练习,我们认识了 ValueError、TypeError、ZeroDivisionError、KeyError、IndexError、FileNotFoundError 和 NameError 等异常。
  • 主动退出与异常故障:SystemExit 可以请求 Python 解释器退出,它与 ValueError、ZeroDivisionError 等普通程序错误的含义不同。

对于 Linux 运维自动化来说,理解这些知识很重要。一个脚本不仅要能够完成正常任务,还应该考虑用户输入错误、文件缺失、网络请求失败等情况,并且能够让其他自动化工具正确判断执行结果。

下一篇,我们正式学习 Python 异常处理的两个基本子句:try 和 except,从捕获 ValueError 开始,让程序遇到错误时能够按照我们预先设计的方式处理。