Python 系列(四十二) ValueError、TypeError 到底是什么?一文搞懂常见异常
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 系列:使用 raise 主动引发异常,让程序学会检查数据
上一篇我们认识了 Python 的常见异常类型,学习了 ValueError、TypeError 等异常的区别,也了解了如何使用 try 和 except 捕获异常。
不过,异常通常有两种来源。
一种是 Python 在执行代码时发现了问题。例如,使用 int("abc") 将无法转换的字符串转成整数,就会产生 ValueError。
另一种是程序本身有明确的业务规则。例如,年龄不能为负数。虽然 -5 是合法的整数,但它不符合我们的业务要求。
这时,我们就需要使用 raise 主动引发异常。
这篇文章就从一个简单的年龄检查程序开始,学习如何使用 raise 检查数据、主动引发异常,以及配合 try 和 except 完整处理输入错误。
为什么需要主动引发异常
先看一个简单的例子。
假设我们希望编写一个程序,让用户输入年龄,并输出输入的结果。
创建 raise_demo.py,写入下面的代码:
1 | age = int(input("请输入年龄:")) |
运行程序:
1 | python3 raise_demo.py |
如果输入 18,程序会正常输出:
1 | 请输入年龄:18 |
但如果输入 abc,程序就会报错。
原因是 input() 读取到的内容始终是字符串,而 int() 无法将 "abc" 转换为整数,因此会产生 ValueError。
还有一种情况:如果输入 -5,程序却可以正常输出:
1 | 请输入年龄:-5 |
这是因为对于 Python 来说,-5 是合法的整数。int() 只负责转换数据类型,并不知道年龄不能为负数这样的业务规则。
因此,我们需要在程序中主动检查输入数据。
使用 raise 主动引发异常
Python 提供了 raise 语句,用于主动引发异常。
例如:
1 | age = -5 |
运行程序后,会产生 ValueError,异常信息为:
1 | Traceback (most recent call last): |
这段代码可以分成三个部分理解。
if age < 0:检查年龄是否小于零。raise ValueError("年龄不能为负数"):如果条件成立,就主动引发ValueError。print(...):只有前面的代码没有引发异常,程序才会继续执行到这里。
这里的 raise 可以理解为程序主动发出的一个信号:当前数据不符合要求,需要停止正常流程,并交由异常处理机制处理。
需要注意,raise 并不是用来修复错误的。它负责指出问题,具体如何处理,则取决于程序是否编写了相应的异常处理代码。
补全异常捕获,让程序能够处理输入错误
前面的例子还有一个问题。
如果用户输入 abc,程序会因为 int() 转换失败而报错;如果用户输入 -5,程序又会因为我们主动引发的异常而报错。
在实际应用中,我们通常不希望程序直接把错误信息和调用栈显示给普通用户,而是希望给出清晰的提示,让用户有机会重新输入。
这就需要把 raise 与之前学习的 try 和 except 结合起来。
将 raise_demo.py 修改为下面的完整代码:
1 | try: |
运行程序:
1 | python3 raise_demo.py |
我们分别测试几种不同的输入。
输入正常的年龄
输入 18:
1 | 请输入年龄:18 |
程序的执行过程如下:
input()读取输入内容"18"。int()将字符串转换为整数18。age < 0的判断结果为False,不会执行raise。try代码块正常执行结束,程序进入else,输出年龄。
输入非数字内容
输入 abc:
1 | 请输入年龄:abc |
这次,程序在执行 int(input(...)) 时就产生了 ValueError。
因为异常发生在 try 代码块内部,Python 会跳过该代码块中剩余的语句,查找匹配的 except。
这里的 except ValueError as e 捕获了异常,并将异常对象赋值给变量 e。通过 print(f"输入错误:{e}"),程序就可以显示异常所携带的信息。
输入负数
输入 -5:
1 | 请输入年龄:-5 |
这次,int() 转换成功,程序继续执行年龄检查。
由于 age < 0 的判断结果为 True,程序执行:
1 | raise ValueError("年龄不能为负数") |
主动引发 ValueError。
随后,程序进入同一个 except ValueError as e 分支,输出我们自定义的错误信息。
这就说明,自动产生的异常和使用 raise 主动引发的异常,都可以由匹配的 except 捕获。
不过,这里也有一个值得注意的地方:非数字输入和负数输入虽然原因不同,但在这个示例中都使用了 ValueError。因此,捕获后需要根据实际需求决定如何向用户解释问题。
如果希望给两种错误显示不同的提示,可以将输入转换和业务检查分别处理。
让不同的错误显示不同的提示
在刚才的例子中,输入 abc 时,Python 返回的是比较技术化的错误信息:
1 | invalid literal for int() with base 10: 'abc' |
对于刚接触程序的用户来说,这样的信息不够直观。
我们可以把数据转换和业务检查分开,分别给出更清晰的提示:
1 | try: |
运行后,输入不同内容会得到不同的结果。
输入 abc:
1 | 请输入年龄:abc |
输入 -5:
1 | 请输入年龄:-5 |
输入 18:
1 | 请输入年龄:18 |
这里使用了两个 try 代码块。
第一个负责将输入转换为整数。如果转换失败,就输出格式错误提示。
只有转换成功后,程序才会进入外层的 else,继续检查年龄是否符合业务规则。第二个 try 用于捕获负数检查过程中主动引发的 ValueError。
这种写法能清楚地区分两类问题,但代码也更长了。对于简单的入门示例,前面使用一个 try 和一个 except 的版本通常更容易理解;如果业务逻辑越来越复杂,再根据实际需要拆分处理过程。
raise 和 except 是如何配合工作的
现在,我们已经知道如何使用 raise 主动引发异常,也知道如何使用 except 捕获异常。
可以把两者的关系理解为:
raise:发现不符合要求的情况后,主动引发异常。except:发现有匹配的异常后,执行相应的处理逻辑。
它们可以配合起来,让程序在发现问题时及时停止当前的正常执行流程,并转入异常处理流程。
下面用流程图梳理完整过程。
flowchart TD
A["开始执行程序"] --> B["读取用户输入"]
B --> C["使用 int() 转换为整数"]
C --> D{"转换是否成功?"}
D -->|否| E["自动产生 ValueError"]
D -->|是| F{"年龄是否小于 0?"}
F -->|是| G["raise 主动引发 ValueError"]
F -->|否| H["进入 else,输出年龄"]
E --> I["except 捕获 ValueError"]
G --> I
I --> J["输出错误提示"]
H --> K["程序正常结束"]
J --> K
classDef normal fill:#e3f2fd,stroke:#1976d2,color:#0d47a1;
classDef decision fill:#fff3e0,stroke:#ef6c00,color:#e65100;
classDef error fill:#ffebee,stroke:#c62828,color:#b71c1c;
classDef endNode fill:#e8f5e9,stroke:#388e3c,color:#1b5e20;
class A,B,C,H normal;
class D,F decision;
class E,G,I,J error;
class K endNode;这张图里有两条需要重点理解的路径:
- 转换失败:输入非数字内容,
int()自动产生ValueError,程序进入except。 - 业务检查失败:整数转换成功,但年龄小于零,程序通过
raise主动引发ValueError,随后同样进入except。
如果输入合法整数,程序就不会引发异常,而是进入 else,输出正常结果。
raise 什么时候适合使用
raise 常用于程序已经发现某个条件不符合要求,需要明确报告问题的场景。
例如:
- 年龄不能为负数。
- 端口号必须处于有效范围。
- 配置文件中必须包含某个必填参数。
- 调用某个业务功能时,传入的参数不符合要求。
不过,不是所有不合法输入都必须通过 raise 处理。
例如,用户输入非数字内容时,程序可以捕获 ValueError,提示用户重新输入。如果业务代码发现一个不可能成立的条件,也可以主动引发异常,避免程序继续使用错误的数据。
关键在于:先明确什么情况属于错误,再决定应该使用哪种异常,以及由哪一层代码负责处理。
写在最后
通过这篇文章,我们学习了如何使用 raise 主动引发异常,并将它与 try、except 和 else 结合起来,完成一个可以处理输入错误的年龄检查程序。
现在,你应该能够区分两种常见情况:
- Python 在执行操作时自动产生异常,例如将
"abc"转换为整数时产生ValueError。 - 程序根据业务规则主动引发异常,例如发现年龄为负数时使用
raise ValueError(...)。
同时,我们还补全了异常捕获流程,分别处理非数字输入、负数输入和正常输入。
下一篇,我们将学习如何定义自己的异常类型,让程序能够使用更贴近实际业务含义的异常来描述问题。
