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 的异常处理,认识了 try、except、else 和 finally,还通过 CPU 使用率检查的例子,学会了处理输入格式错误和数值范围错误。
不过,你可能已经发现一个问题:Python 中的异常类型有很多。
输入 abc,可能遇到 ValueError;使用了不支持的操作,可能遇到 TypeError;访问不存在的列表位置,可能遇到 IndexError;打开不存在的文件,则可能遇到 FileNotFoundError。
为什么 Python 要设计这么多异常类型?遇到不同的问题时,我们又应该选择哪一种异常进行处理?
这篇文章就从实际问题出发,逐步认识 Python 的常见异常类型、异常继承关系,以及异常处理时的选择思路。
为什么需要不同的异常类型
先看一个简单的例子。
假设我们编写了一个服务器监控程序,需要将用户输入的 CPU 使用率转换为整数。
创建或打开 cpu_check.py,写入下面的代码:
1 | cpu_usage = int(input("请输入 CPU 使用率:")) |
运行程序:
1 | python3 cpu_check.py |
输入 abc,程序会因为无法将字符串转换为整数而产生 ValueError 异常。
如果我们尝试把字符串和整数直接相加:
1 | result = "CPU 使用率:" + 75 |
程序则会产生 TypeError 异常,因为这里的加法操作不支持字符串和整数直接相加。
这两个例子都属于程序运行时遇到的问题,但原因并不相同。
ValueError:传入的数据类型符合要求,但数据的值不合适。TypeError:操作使用了不合适的数据类型,或者传入了不支持的参数类型。
如果所有问题都使用同一种异常类型,程序就很难准确判断到底发生了什么。
因此,Python 为不同的问题设计了不同的异常类型。这样既方便开发者定位问题,也可以让程序根据不同异常采取不同的处理方式。
遇到异常时,应该如何选择
刚开始学习异常处理时,很容易陷入一个误区:看到程序报错,就随便找一个 except 把它捕获。
但异常处理并不是简单地把错误隐藏起来。我们首先要判断问题发生在哪里、为什么发生,再决定如何处理。
例如:
- 用户输入了无法转换为整数的内容,可以考虑捕获
ValueError。 - 程序访问了不存在的列表位置,可以考虑捕获
IndexError。 - 程序读取的文件不存在,可以考虑捕获
FileNotFoundError。 - 程序执行了不支持的数据类型操作,可以考虑捕获
TypeError。
可以把这个过程理解为下面的流程。
flowchart TD
A["程序执行过程中出现问题"] --> B{"问题发生在哪个环节?"}
B --> C["数据转换或参数取值"]
B --> D["数据类型或操作不匹配"]
B --> E["列表或字典查找"]
B --> F["文件或系统资源访问"]
B --> G["其他问题"]
C --> C1{"具体原因是什么?"}
C1 -->|值无法转换或取值不合法| H["考虑 ValueError"]
D --> I["考虑 TypeError"]
E --> J{"查找对象是什么?"}
J -->|列表位置不存在| K["考虑 IndexError"]
J -->|字典键不存在| L["考虑 KeyError"]
F --> M{"具体原因是什么?"}
M -->|文件不存在| N["考虑 FileNotFoundError"]
M -->|权限或其他系统错误| O["考虑相应的 OSError 子类"]
G --> P["查看异常名称和完整报错信息"]
H --> Q["确定是否需要捕获"]
I --> Q
K --> Q
L --> Q
N --> Q
O --> Q
P --> Q
Q --> R{"程序能否合理处理该异常?"}
R -->|能| S["使用具体的 except 处理"]
R -->|不能| T["修正问题或让异常继续向上传递"]
classDef start fill:#e3f2fd,stroke:#1976d2,color:#0d47a1;
classDef decision fill:#fff3e0,stroke:#ef6c00,color:#e65100;
classDef exception fill:#e8f5e9,stroke:#388e3c,color:#1b5e20;
classDef endNode fill:#ede7f6,stroke:#673ab7,color:#311b92;
class A start;
class B,C1,J,M,R decision;
class H,I,K,L,N,O,P exception;
class S,T endNode;这张图展示的是常见问题的排查思路,并不是要求我们在看到每一种错误时,都必须使用 try 和 except。
有些错误应该由程序处理,例如用户输入不合法;有些错误则应该从根本上修复,例如代码使用了错误的数据类型。
选择异常类型的关键,是先理解问题的原因,再选择合适的处理方式,而不是为了让程序不报错就随意捕获异常。
从几个常见的异常类型开始
理解异常类型最好的办法,不是死记名称,而是结合代码观察:什么情况下会产生异常,Python 又会给出什么类型的异常。
下面统一使用 cpu_check.py 作为示例文件。每次测试时,将对应代码放入文件中保存,再执行:
1 | python3 cpu_check.py |
ValueError:值不合适
先看一个例子:
1 | cpu_usage = int("abc") |
运行后,Python 会报告 ValueError。
原因是 int() 可以把 "75" 这样的数字字符串转换为整数,但无法把 "abc" 转换为整数。
1 | print(int("75")) |
输出:
1 | 75 |
而下面的代码无法正常执行:
1 | print(int("abc")) |
因此,当数据的值不符合某个操作的要求时,就可能出现 ValueError。
需要注意,ValueError 并不只用于数字转换。例如,某些函数接收了类型正确、但取值不合法的参数时,也可能产生这种异常。
TypeError:类型不合适
再看下面的代码:
1 | cpu_usage = 75 |
运行后,会产生 TypeError。
为什么?
因为 "CPU 使用率:" 是字符串,而 cpu_usage 是整数。Python 不允许直接使用 + 将这两种数据相加。
如果我们希望把整数转换为字符串,可以使用 str():
1 | cpu_usage = 75 |
输出:
1 | CPU 使用率:75 |
这里,str(cpu_usage) 将整数转换成字符串,两个字符串就可以使用 + 拼接了。
这个例子说明,TypeError 通常意味着操作或函数调用使用了不合适的数据类型。
IndexError:列表位置不存在
假设我们需要保存几台服务器的名称:
1 | servers = ["web01", "web02", "web03"] |
输出:
1 | web01 |
这里的 servers 是一个列表,可以把它理解为一个按照顺序存放多个数据的容器。
Python 列表的位置编号从 0 开始,而不是从 1 开始。
因此,这个列表的有效位置是 0、1 和 2。
如果继续访问位置 3:
1 | servers = ["web01", "web02", "web03"] |
程序就会产生 IndexError,因为列表中没有这个位置。
KeyError:字典中的键不存在
在运维工作中,我们可能需要记录服务器名称和 IP 地址之间的对应关系。
例如:
1 | servers = { |
输出:
1 | 192.168.1.10 |
这里使用了 Python 字典。字典可以通过一个键查找对应的值,例如通过服务器名称查找 IP 地址。
如果访问不存在的键:
1 | servers = { |
程序就会产生 KeyError,因为字典中不存在 "web04" 这个键。
如果只是希望查询某台服务器的 IP 地址,并且允许服务器不存在,可以使用 get():
1 | servers = { |
输出:
1 | 未找到服务器 |
get() 的第二个参数表示:如果找不到指定的键,就返回这个默认值。
因此,查询字典时并不一定要通过捕获 KeyError 来解决问题。根据实际需求选择合适的方法,往往更加简单。
FileNotFoundError:文件不存在
假设我们需要读取一份服务器清单:
1 | with open("servers.txt", "r", encoding="utf-8") as file: |
这段代码尝试打开当前工作目录中的 servers.txt 文件。
如果文件不存在,Python 通常会产生 FileNotFoundError。
这里的 with open() 用于打开文件,并在离开代码块时自动关闭文件。暂时不必深入理解它的全部语法,只需要知道 open() 会尝试访问指定的文件即可。
文件不存在只是可能的原因之一。如果文件存在,但当前用户没有足够的访问权限,也可能遇到其他异常,例如 PermissionError。
用 try 和 except 处理不同的异常
前面我们认识了 ValueError 和 TypeError 等异常类型。现在的问题是:如果程序遇到不同的问题,能不能分别给出不同的提示?
当然可以。我们可以在同一个 try 代码块后面编写多个 except 子句,让 Python 根据实际发生的异常,选择对应的处理逻辑。
先看下面的例子。将代码保存到 cpu_check.py:
1 | try: |
运行程序:
1 | python3 cpu_check.py |
测试 ValueError
输入 abc:
1 | 请输入 CPU 使用率:abc |
程序执行到 int(input(...)) 时,发现 abc 无法转换为整数,于是产生 ValueError。
Python 找到匹配的 except ValueError,执行对应的错误处理代码。
由于异常发生在 try 代码块内部,后面的字符串拼接语句不会执行。
测试 TypeError
再次运行:
1 | python3 cpu_check.py |
输入 75:
1 | 请输入 CPU 使用率:75 |
这次整数转换成功了,但接下来执行:
1 | result = "当前 CPU 使用率为:" + cpu_usage |
左边是字符串,右边是整数,Python 不支持直接使用 + 拼接这两种不同类型的数据,因此产生 TypeError。
程序于是进入 except TypeError 分支。
如果我们希望这段代码正常工作,可以先把整数转换为字符串:
1 | result = "当前 CPU 使用率为:" + str(cpu_usage) |
不过,在这个示例中,我们故意保留了类型不匹配的问题,以便观察 TypeError 是如何被捕获的。
理解多个 except 的执行规则
这个例子展示了两种不同的异常:
- 输入
abc:整数转换失败,触发ValueError。 - 输入
75:整数转换成功,但字符串与整数直接拼接,触发TypeError。
Python 会按照异常发生时的实际类型,查找能够匹配的 except 子句。匹配成功后执行对应分支,不会继续执行后面的其他 except 分支。
在实际项目中,应该优先捕获自己能够合理处理的具体异常,而不是为了隐藏错误而捕获所有异常。这样既方便定位问题,也能避免把不同原因的错误混在一起处理。
理解 Python 异常的继承关系
前面我们学习了多种异常类型。接下来,再深入一步:它们之间其实存在继承关系。
如果你还记得之前学习的 Python 类与继承,就可以把异常类型理解为一组有层次的类。
例如,FileNotFoundError 不仅是一种具体的异常类型,它还属于更广泛的 OSError 类型。
这意味着,如果我们使用 except OSError,就可以捕获 FileNotFoundError 这类属于 OSError 的异常。
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,stroke-width:2px;
classDef main fill:#e3f2fd,stroke:#1976d2,color:#0d47a1,stroke-width:1.5px;
classDef branch fill:#e8f5e9,stroke:#388e3c,color:#1b5e20;
classDef leaf fill:#fff3e0,stroke:#ef6c00,color:#e65100;
class A root;
class B main;
class C,D,E main;
class F,G,H,K,L,M,N branch;
class F1,F2,F3,G1,G2,H1,H2,H3,I,J,O,K1,L1,M1,N1 leaf;这张图展示的是常见异常类型之间的简化继承关系,并不是 Python 所有异常类的完整列表。
可以重点记住以下几组关系:
ZeroDivisionError属于ArithmeticError,用于表示某些除零错误。IndexError和KeyError都属于LookupError,但分别用于不同的查找场景。FileNotFoundError和PermissionError都属于OSError。ModuleNotFoundError属于ImportError。UnboundLocalError属于NameError。IndentationError属于SyntaxError。
其中,BaseException 是 Python 内置异常体系的根类。常规应用程序一般从 Exception 及其子类开始处理异常,而不是直接捕获 BaseException。
这是因为 KeyboardInterrupt 和 SystemExit 等类型也直接继承自 BaseException,它们通常不应该被普通的业务异常处理逻辑随意拦截。
为什么异常继承关系很重要
理解继承关系后,我们就能写出更灵活的异常处理代码。
例如,打开文件时可能遇到 FileNotFoundError。我们可以专门处理这种异常:
1 | try: |
也可以捕获更广泛的 OSError:
1 | try: |
第二种写法可以捕获 FileNotFoundError,也可以捕获其他属于 OSError 的异常,例如 PermissionError。
不过,捕获范围越宽,并不代表代码就越好。如果不同异常需要不同的处理方式,应该优先使用更具体的异常类型;如果多种相关异常确实可以使用同一种处理方式,再考虑捕获它们共同的父类。
使用 issubclass() 验证异常的继承关系
我们还可以使用 Python 内置函数 issubclass() 来检查两个类之间是否存在继承关系。
例如:
1 | print(issubclass(FileNotFoundError, OSError)) |
将代码保存到 cpu_check.py,然后运行:
1 | python3 cpu_check.py |
输出:
1 | True |
为什么会出现这样的结果?
FileNotFoundError是OSError的子类,因此结果为True。IndexError是LookupError的子类,因此结果为True。KeyError也是LookupError的子类,因此结果为True。ValueError不属于LookupError的继承体系,因此结果为False。
这里的 issubclass(子类, 父类) 用于判断前面的类是否继承自后面的类,或者是否通过其他继承路径属于该类的子类。
注意,issubclass() 判断的是类之间的关系,而不是某个变量当前保存的数据属于什么类型。后者可以使用之前学过的 isinstance()。
写在最后
通过这篇文章,我们认识了 ValueError、TypeError、IndexError、KeyError 和 FileNotFoundError 等常见异常,了解了它们分别适用于哪些场景。
我们还通过异常选择流程图,梳理了从判断问题原因到选择异常类型、再到决定如何处理的基本思路。
此外,我们学习了 Python 异常的继承关系,知道了异常不仅有具体类型,还可以通过父类组织成层次结构。利用这种关系,程序既可以捕获具体异常,也可以根据需要捕获一组相关异常。
实际编程时,不必死记所有异常类型。更重要的是,遇到问题时能够读懂异常名称和报错信息,判断错误发生的原因,再选择合适的处理方式。
下一篇,我们将学习如何使用 raise 主动引发异常,让程序在发现不符合业务规则的情况时,主动报告问题,而不只是等待 Python 自动产生异常。
