Python 系列(四十) 从零学会异常处理,掌握 try、except、else 和 finally
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 中的异常,知道了程序运行时可能遇到各种问题。例如,用户输入的内容不是数字,程序尝试将它转换为整数时,就可能出现 ValueError 异常。
那么,问题来了:如果程序遇到了异常,能不能不要直接报错退出,而是给出友好的提示,让程序有机会继续执行后续任务?
当然可以。Python 提供了 try 和 except,帮助我们捕获并处理异常。
这篇文章将从一个简单的 CPU 使用率检查程序开始,逐步学习 try、except、else 和 finally,并进一步实现实际的输入范围校验。
从一个实际问题开始
假设我们正在编写一个简单的服务器监控工具,需要用户输入 CPU 使用率,再根据输入的数值判断服务器是否存在性能风险。
先创建一个名为 cpu_check.py 的文件。
如果你使用 Linux,可以执行下面的命令创建文件:
1 | touch cpu_check.py |
然后使用自己熟悉的编辑器打开这个文件,例如:
1 | vim cpu_check.py |
写入下面的代码:
1 | cpu_usage = int(input("请输入 CPU 使用率:")) |
保存文件后,在文件所在目录运行:
1 | python3 cpu_check.py |
如果输入 75,程序会输出:
1 | 请输入 CPU 使用率:75 |
看起来一切正常。
但是,如果输入的不是数字,而是 abc 呢?
1 | 请输入 CPU 使用率:abc |
程序就会报错。因为 input() 接收到的内容是字符串,而 int() 无法将 abc 转换为整数,所以 Python 会产生 ValueError 异常。
这里还存在另一个问题。
如果用户输入 999,程序虽然不会因为整数转换而报错,却会把 CPU 使用率显示为 999%。
这显然不符合我们的预期。对于通常意义上的 CPU 使用率百分比,合理的输入范围应该是 0 到 100。
因此,我们需要解决两个不同的问题:
- 输入格式检查: 用户输入的内容能不能转换为整数?
- 输入范围检查: 转换成功后,这个整数是否在合理范围内?
第一个问题可以通过异常处理来解决,第二个问题则需要通过条件判断来完成。
使用 try 和 except 捕获异常
先解决输入格式的问题。
修改 cpu_check.py 中的代码:
1 | try: |
保存后,仍然使用同一条命令运行:
1 | python3 cpu_check.py |
当输入 75 时,输出如下:
1 | 请输入 CPU 使用率:75 |
当输入 abc 时,输出如下:
1 | 请输入 CPU 使用率:abc |
这次,程序没有直接显示异常追踪信息,而是执行了我们提前编写的错误处理代码。
这是怎么做到的?
try:尝试执行可能发生异常的代码
try 可以理解为:先尝试执行这里的代码,如果发生异常,就由后面的异常处理逻辑决定如何应对。
例如:
1 | try: |
这里有两个操作:
input()接收用户输入。int()尝试将输入内容转换为整数。
如果用户输入 75,转换成功,程序继续执行 try 中的下一条语句。
如果用户输入 abc,转换就会产生 ValueError 异常。
发生异常后,Python 不会继续执行当前 try 代码块中尚未执行的语句,而是开始查找能够处理这个异常的 except 子句。
except:处理指定类型的异常
接下来是:
1 | except ValueError: |
这里的 except ValueError 表示:如果前面的 try 代码发生了 ValueError 异常,就执行下面缩进的代码。
我们可以这样理解:
try:尝试执行代码。except ValueError:如果发生匹配的ValueError异常,就执行指定的处理代码。
需要注意,except ValueError 并不是处理所有异常的万能语句。它主要用于捕获 ValueError 及其子类异常。
例如,如果程序因为变量名称写错而产生 NameError,这个 except ValueError 就不会处理它。没有其他匹配的异常处理逻辑时,该异常仍然会向外传播。
因此,编写异常处理代码时,应该根据实际可能出现的问题,选择合适的异常类型,而不是简单地把所有异常都忽略掉。
理解异常发生后的执行顺序
异常处理最重要的地方,不只是记住语法,还要理解程序的执行顺序发生了什么变化。
继续修改 cpu_check.py:
1 | try: |
运行:
1 | python3 cpu_check.py |
输出结果:
1 | 开始检查服务器 |
为什么没有输出 CPU 使用率检查完成?
因为程序执行到 int("abc") 时产生了 ValueError 异常。Python 随即跳过当前 try 代码块中剩余的语句,转而执行能够匹配该异常的 except 分支。
异常处理完成后,程序继续执行整个异常处理结构后面的代码,因此最后的 print() 仍然能够执行。
执行过程可以概括为:
flowchart TD
A["进入 try 代码块"] --> B["执行代码"]
B --> C{"是否发生异常?"}
C -->|没有| D["继续执行 try 中的代码"]
D --> E["跳过 except"]
C -->|发生匹配的异常| F["跳过 try 中剩余的代码"]
F --> G["执行对应的 except"]
E --> H["继续执行异常处理结构后面的代码"]
G --> H需要注意,异常被捕获并处理,并不意味着程序中所有问题都已经解决。异常处理能否让程序继续工作,取决于我们编写的处理逻辑。
使用 else 处理正常执行的情况
有时候,我们希望把正常情况下要执行的代码,与异常处理代码分开。
Python 提供了 else 子句。
else 只有在 try 代码块没有发生异常时才会执行。
修改 cpu_check.py:
1 | try: |
运行:
1 | python3 cpu_check.py |
输入 75:
1 | 请输入 CPU 使用率:75 |
输入 abc:
1 | 请输入 CPU 使用率:abc |
可以看到:
- 输入
75,整数转换成功,没有发生异常,因此执行else。 - 输入
abc,产生ValueError,执行except,跳过else。
这里有一个容易误解的地方:else 判断的是 try 代码块是否正常结束,而不是判断用户输入的内容是否符合所有业务要求。
例如,输入 999 时,int("999") 可以正常执行,所以 else 仍然会执行。
这就是为什么我们还需要额外进行输入范围校验。
另外,else 是可选的。如果没有需要单独放在正常分支中的代码,就不必使用它。
使用 finally 执行收尾工作
在实际运维工作中,程序可能需要打开文件、连接数据库或者访问远程服务器。
这些操作可能涉及需要在使用完毕后释放的资源。即使执行过程中发生异常,我们通常也希望有机会完成必要的收尾工作。
Python 提供了 finally 子句。
finally 中的代码通常无论 try 是否发生异常都会执行。
修改 cpu_check.py:
1 | try: |
运行:
1 | python3 cpu_check.py |
输入正确的数字:
1 | 请输入 CPU 使用率:75 |
输入错误的内容:
1 | 请输入 CPU 使用率:abc |
无论输入是否正确,最后都会执行 finally 中的语句。
不过,finally 并不是绝对保证执行的。例如,进程被强制终止或者机器突然断电时,程序可能没有机会执行它。
此外,finally 也不能代替正确的资源管理方式。对于文件等资源,后续还可以学习更适合的 with 语句。
正确理解 try、except、else 和 finally 的结构
现在,我们已经认识了四个关键字。接下来,把它们放在一起理解。
1 | try: |
这里的 pass 是 Python 的占位语句,表示暂时不执行任何操作。它可以帮助我们先搭建代码结构,再逐步补充具体逻辑。
四个部分的作用如下:
| 关键字 | 作用 | 使用要求 |
|---|---|---|
try | 放置可能发生异常的代码 | 必须 |
except | 捕获并处理匹配的异常 | 可以有一个或多个;也可以与 finally 组合使用 |
else | 在 try 没有发生异常时执行 | 可选,必须与至少一个 except 配合使用 |
finally | 执行通常需要完成的收尾操作 | 可选 |
需要特别注意,except 和 finally 并不是必须同时出现的。Python 支持 try-except、try-finally、try-except-else 以及 try-except-finally 等合法结构。
但 try 不能单独构成完整的异常处理语句,必须搭配至少一个 except 或一个 finally。
实战:校验 CPU 使用率的输入范围
现在,我们把前面学到的知识真正用起来。
假设要编写一个简单的 CPU 使用率检查程序,需要满足以下要求:
- 用户必须输入一个整数。
- CPU 使用率的合理范围是
0到100,包括两个边界值。 - 如果输入的不是整数,提示用户输入格式错误。
- 如果输入整数但超出范围,提示用户输入范围错误。
- 如果输入合法,则显示 CPU 使用率,并判断是否需要关注。
这里要区分两类问题:
| 用户输入 | 是否能转换为整数 | 是否在合理范围内 |
|---|---|---|
75 | 是 | 是 |
abc | 否 | 无法进行整数范围校验 |
999 | 是 | 否 |
-5 | 是 | 否 |
0 | 是 | 是 |
100 | 是 | 是 |
abc 无法转换为整数,因此需要通过 except ValueError 处理。
999 和 -5 都是合法的整数,但不符合 CPU 使用率的业务范围,需要通过 if 条件判断处理。
将 cpu_check.py 的内容替换为下面的完整代码:
1 | try: |
运行命令保持不变:
1 | python3 cpu_check.py |
测试正常输入
输入 75:
1 | 请输入 CPU 使用率(0-100):75 |
因为 75 可以转换为整数,并且处于 0 到 100 之间,所以程序正常显示 CPU 使用率。
测试超出范围的输入
输入 999:
1 | 请输入 CPU 使用率(0-100):999 |
这次没有发生异常,因为 999 本身就是一个整数。
程序通过下面的条件判断发现它超出了合理范围:
1 | if 0 <= cpu_usage <= 100: |
当条件不成立时,程序进入 else 分支,显示范围错误提示。
测试无法转换为整数的输入
输入 abc:
1 | 请输入 CPU 使用率(0-100):abc |
此时,int() 转换失败,产生 ValueError,程序进入 except 分支。
由于异常发生在 try 中,后面的 else 不会执行,但 finally 中的收尾语句仍会执行。
理解这段程序的执行顺序
flowchart TD
A["接收用户输入"] --> B["int() 尝试转换为整数"]
B --> C{"转换是否成功?"}
C -->|否| D["except:提示输入格式错误"]
C -->|是| E["进入 else,检查数值范围"]
E --> F{"是否在 0 到 100 之间?"}
F -->|否| G["提示输入范围错误"]
F -->|是| H["显示 CPU 使用率"]
H --> I{"CPU 使用率是否达到 80%?"}
I -->|是| J["提示 CPU 使用率较高"]
I -->|否| K["提示 CPU 使用率正常"]
D --> L["finally:输出流程结束"]
G --> L
J --> L
K --> L通过这个例子,我们可以看到,异常处理和条件判断解决的是不同的问题。
异常处理负责应对执行过程中出现的异常,例如无法将字符串转换为整数;条件判断负责检查业务规则,例如 CPU 使用率是否处于合理范围。
两者结合,才能让程序更加健壮。
需要注意,这里使用的 80% 只是演示用的告警阈值,并不是适用于所有服务器和业务的统一标准。
写在最后
通过这篇文章,我们学习了 Python 最基础的异常处理结构:
try:尝试执行可能发生异常的代码。except:捕获并处理匹配的异常。else:在try没有发生异常时执行。finally:执行通常需要完成的收尾工作。
我们还通过 CPU 使用率检查的例子,区分了输入格式校验和输入范围校验。
这两个概念在实际编程中非常重要:能够转换为正确的数据类型,不代表输入的数据就符合业务要求。
异常处理也不意味着把所有错误都隐藏起来。合理的做法是识别可能出现的问题,选择合适的异常类型,并给出有帮助的提示。对于没有预料到的程序错误,不应该随意忽略。
下一篇,我们将进一步学习 Python 的异常继承关系,理解为什么 ValueError、TypeError 等异常属于不同的类型,以及 Python 为什么允许我们按照异常类型进行捕获。
