Python 系列(三十九) 搞懂异常模型与常见异常类型,读懂程序报错
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 的类和对象,也认识了
type()、isinstance()和issubclass()等内置函数。到目前为止,我们已经能够编写一些简单的程序,让它们按照预期完成任务。
但是,在实际工作中,程序并不总是能顺利运行。
比如,我们编写了一个 Python 脚本,用来统计服务器的 CPU 使用率。程序要求输入一个数字,结果有人输入了
abc;又或者,我们需要读取一个日志文件,但文件实际上并不存在。这些情况都可能导致程序报错。如果程序一遇到错误就直接退出,那么自动化运维脚本还能可靠地工作吗?
为了让程序能够更好地应对意外情况,我们需要先理解 Python 的异常模型,弄清楚异常是如何产生的、不同异常之间有什么关系,以及异常发生后程序究竟会发生什么。
从一个实际问题开始
假设我们正在编写一个简单的服务器监控程序,需要用户输入 CPU 使用率,然后判断服务器是否需要告警。
先创建一个名为 cpu_check.py 的文件,写入下面的代码:
1 | cpu_usage = input("请输入服务器的 CPU 使用率:") |
这段代码暂时不用急着完全理解。我们先关注它的运行过程。
打开终端,进入保存 cpu_check.py 的目录,然后执行:
1 | python cpu_check.py |
如果输入 65,程序会输出:
1 | 请输入服务器的 CPU 使用率:65 |
如果输入 90,程序会输出:
1 | 请输入服务器的 CPU 使用率:90 |
看起来一切正常。
但是,如果有人输入 abc 呢?
1 | 请输入服务器的 CPU 使用率:abc |
程序报错了,而且后面的 if 判断也没有执行。
问题出在这一行:
1 | cpu_usage = float(cpu_usage) |
前面我们学过,input() 接收用户输入的内容,并以字符串的形式返回。
因此,即使用户输入的是 65,input() 返回的也不是整数或浮点数,而是字符串 "65"。
float() 可以把合法的数字字符串转换成浮点数:
1 | print(float("65")) |
输出:
1 | 65.0 |
但是,"abc" 不是合法的数字字符串,Python 无法完成转换,于是引发了 ValueError 异常。
为什么 Python 会产生这种错误?ValueError 又是什么?为什么它会导致后续代码不再执行?
要回答这些问题,我们先从异常的基本概念开始。
什么是异常
异常(Exception)是 Python 在执行程序时,用来表示错误或其他异常情况的一种机制。
当 Python 执行某条语句时,如果发现当前操作无法正常完成,就可能引发异常。
例如,执行下面的代码:
1 | print(10 / 0) |
除法运算的除数不能为零,因此 Python 会引发 ZeroDivisionError 异常。
再看一个例子:
1 | number = int("hello") |
int() 可以把合法的整数形式字符串转换成整数,但 "hello" 不是合法的整数表示,因此 Python 会引发 ValueError 异常。
还有一个例子:
1 | with open("server.log", "r", encoding="utf-8") as file: |
这段代码尝试读取当前目录下的 server.log 文件。如果文件不存在,Python 通常会引发 FileNotFoundError 异常。
这几个例子发生的原因不同:
- 除数为零,属于
ZeroDivisionError。 - 数值转换失败,属于
ValueError。 - 文件不存在,属于
FileNotFoundError。
它们都属于 Python 的异常体系。
异常的意义,不只是告诉我们程序出错了,还可以让程序识别问题的类型,并决定如何应对。
不过,异常并不一定意味着程序员写错了代码。用户输入了不符合要求的数据、文件暂时不存在、网络请求失败,都可能是程序需要考虑的意外情况。
语法错误与异常有什么区别
Python 初学者经常把所有报错都叫作异常,但从 Python 的概念来看,语法错误和运行过程中引发的异常需要区分开来。
语法错误:代码不符合规则
先看下面的代码:
1 | if 80 > 60 |
第一行的条件判断语句末尾缺少了冒号 :。
在 Python 中,if 语句需要使用冒号引出代码块。缺少冒号,就不符合 Python 的语法规则。
执行这个文件时,Python 会报告 SyntaxError。
语法错误意味着代码无法按照 Python 的语法规则被正确解析。对于普通脚本,Python 通常会在执行相关代码前报告这类错误,因此程序无法按预期运行。
修复方式通常是检查拼写、括号、冒号和缩进等语法问题。
异常:程序执行过程中出现问题
再看下面的代码:
1 | cpu_usage = 80 |
这段代码的语法没有问题。
Python 可以理解赋值语句,也可以理解除法表达式。
但是,程序执行到 cpu_usage / 0 时,发现除数为零,于是引发 ZeroDivisionError 异常。
两者的区别可以概括为:
| 对比项 | 语法错误 | 运行时异常 |
|---|---|---|
| 发生原因 | 代码不符合 Python 语法规则 | 执行某个操作时发生错误或异常情况 |
| 常见例子 | SyntaxError | ValueError、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 | print(issubclass(ValueError, Exception)) |
运行程序:
1 | python exception_types.py |
输出结果:
1 | 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 | cpu_usage = float("high") |
思考一下:"high" 能转换成浮点数吗?
运行后会看到类似的错误:
1 | ValueError: could not convert string to float: 'high' |
这里引发的是 ValueError。
原因是传入的对象类型没有问题:float() 本来就可以接收字符串。但字符串的内容不是合法的数字表示,因此转换失败。
记忆方法: 值不符合当前操作的要求,优先考虑 ValueError。
对象类型不合适:TypeError
再看一个场景:
1 | server_port = 8080 |
我们希望把端口号拼接到提示文字后面,但 server_port 是整数,而字符串拼接要求参与拼接的内容是字符串。
运行后会看到类似的错误:
1 | TypeError: can only concatenate str (not "int") to str |
这里引发的是 TypeError。
可以通过显式转换解决这个问题:
1 | server_port = 8080 |
输出:
1 | 服务器端口:8080 |
记忆方法: 操作本身不适用于当前对象类型,优先考虑 TypeError。
除数为零:ZeroDivisionError
监控程序可能需要计算磁盘使用率。如果计算过程中出现除数为零,就会引发异常。
1 | used_space = 80 |
运行后会看到类似的错误:
1 | ZeroDivisionError: division by zero |
这里引发的是 ZeroDivisionError,它属于 ArithmeticError 的子类。
实际开发中,如果 total_space 表示磁盘总容量,那么它为零可能意味着配置或数据有问题。除了处理异常,还应该检查数据来源是否合理。
字典中不存在指定的键:KeyError
假设我们使用字典保存服务器信息:
1 | server = { |
字典中只有 name 和 ip 两个键,没有 cpu。
通过 server["cpu"] 访问不存在的键时,Python 会引发 KeyError。
这是因为字典的方括号访问方式要求指定的键存在。
如果只是希望在键不存在时返回一个默认值,可以考虑使用字典的 get() 方法:
1 | server = { |
输出:
1 | 未知 |
这里没有引发异常,因为 get() 在找不到指定键时会返回我们提供的默认值。
列表索引越界:IndexError
假设我们使用列表保存服务器名称:
1 | servers = ["web01", "web02", "db01"] |
列表只有三个元素,索引分别是 0、1 和 2。访问索引 5 时,超出了列表范围,因此会引发 IndexError。
注意,Python 的列表索引从 0 开始,而不是从 1 开始。
文件不存在:FileNotFoundError
运维脚本经常需要读取日志或配置文件。
1 | with open("missing.log", "r", encoding="utf-8") as file: |
如果当前目录下不存在 missing.log,Python 通常会引发 FileNotFoundError。
它属于 OSError 的子类。
遇到这种问题时,除了检查异常类型,还需要检查文件路径、文件名以及运行脚本时所在的目录。
使用了未定义的变量:NameError
再看下面的代码:
1 | print(server_name) |
如果程序之前没有定义 server_name,Python 就无法找到这个名称对应的变量或其他可用对象,因此会引发 NameError。
需要注意,NameError 与 TypeError 不同。前者是名称无法解析,后者是操作与对象类型不匹配。
练习结果对照
| 代码场景 | 常见异常类型 | 判断依据 |
|---|---|---|
float("high") | ValueError | 字符串内容无法转换成浮点数 |
"端口:" + 8080 | TypeError | 字符串不能直接与整数拼接 |
80 / 0 | ZeroDivisionError | 除数为零 |
server["cpu"],但键不存在 | KeyError | 字典中找不到指定键 |
servers[5],但列表长度不足 | IndexError | 列表索引越界 |
| 打开不存在的文件 | FileNotFoundError | 文件无法找到 |
使用未定义的 server_name | NameError | 名称没有定义 |
这些是常见情况,并不意味着只要出现相似的表面现象就一定是对应异常。实际异常类型还取决于具体代码和执行环境。
Python 报错后,程序究竟发生了什么
现在我们知道了异常的基本概念和继承关系,接下来需要理解另一个容易混淆的问题:
异常发生、程序执行终止和操作系统进程退出,是同一件事吗?
答案是:不是。
异常发生,不代表进程立刻退出
先看一段简单的代码:
1 | print("开始执行") |
运行后,输出大致如下:
1 | 开始执行 |
注意,输出中没有出现“继续执行”。
为什么?
因为程序执行到 10 / 0 时,引发了 ZeroDivisionError。如果没有相应的异常处理逻辑,Python 会中断当前正常的执行流程,寻找能够处理这个异常的代码。
这个寻找处理代码的过程,可以简单理解为:Python 会沿着当前的函数调用关系向外查找,看看有没有合适的异常处理代码。
如果最终找不到能够处理该异常的代码,异常就成为未处理异常。
对于普通 Python 脚本,如果未处理异常发生在主线程中,Python 通常会打印错误追踪信息,并结束主线程的执行。
但这里需要注意:主线程结束,不一定意味着整个操作系统进程在同一时刻退出。
如果进程中还有其他非守护线程在运行,Python 进程仍然可能继续存活,直到这些线程也结束,或者进程通过其他方式退出。
因此,我们不能简单地把“发生异常”理解为“操作系统进程立即退出”。
捕获异常后,程序可以继续执行
再看下面的代码:
1 | print("开始执行") |
这里先不要求你完全理解 try 和 except 的语法,只需要观察程序执行结果。
输出如下:
1 | 开始执行 |
这一次,虽然 10 / 0 仍然引发了 ZeroDivisionError,但是程序中存在可以处理该异常的代码。
Python 执行了异常处理逻辑,处理完成后,又继续执行了后面的 print("继续执行")。
因此,异常并不一定会导致整个程序终止。
是否继续执行,取决于异常有没有被适当地处理,以及处理完成后的程序流程。
异常终止与进程退出的区别
我们把这几个概念放在一起比较。
| 概念 | 含义 | 例子 |
|---|---|---|
| 异常发生 | 某个操作引发异常 | 10 / 0 引发 ZeroDivisionError |
| 当前执行流程中断 | Python 不再按照原来的顺序执行出错位置后面的语句,转而寻找异常处理逻辑 | 未处理异常导致当前调用链退出 |
| 线程执行终止 | 某个线程结束执行 | 主线程因未处理异常而结束 |
| 操作系统进程退出 | 进程生命周期结束,不再运行 | Python 解释器进程退出,操作系统回收相应资源 |
在日常使用的简单 Python 脚本中,如果主线程遇到未处理异常,并且没有其他线程或特殊机制影响进程生命周期,我们通常会观察到:Python 打印异常信息,然后命令执行结束。
但这是常见情形,不是说每次异常都会立即结束整个进程。
如何查看 Python 进程的退出状态
在 Linux 运维中,我们不仅需要观察程序是否报错,还经常需要判断脚本是否执行成功。
操作系统提供了进程退出状态这一机制。对于通过 shell 执行的普通命令,可以使用 $? 查看上一条命令的退出状态。
先创建 exit_status.py:
1 | print("开始执行") |
执行下面的命令:
1 | python exit_status.py |
在常见的 Linux 环境中,输出会包含 ZeroDivisionError 的错误信息,随后显示一个非零退出状态,通常为 1。
例如:
1 | 开始执行 |
这里的 1 表示 Python 进程以非零状态退出,通常意味着执行失败。
但要注意,非零退出状态不是异常对象本身,也不等于异常类型。 它是程序向操作系统报告执行结果的一种方式。
不同程序可以使用不同的非零状态表示失败原因。Python 普通脚本因未处理异常退出时,常见的状态是 1;如果程序显式使用 sys.exit(),也可以指定其他退出状态。
程序主动退出,也不一定是异常故障
Python 还可以通过退出机制主动结束程序。
例如:
1 | import sys |
运行后,程序会打印:
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 | print("步骤一:程序开始") |
执行程序:
1 | python exception_flow.py |
输出结果:
1 | 步骤一:程序开始 |
为什么没有出现“步骤三:计算完成”?
因为 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 开始,让程序遇到错误时能够按照我们预先设计的方式处理。
