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 的异常处理,认识了 try、except、else 和 finally,还通过 CPU 使用率检查的例子,学会了处理输入格式错误和数值范围错误。

不过,你可能已经发现一个问题:Python 中的异常类型有很多。

输入 abc,可能遇到 ValueError;使用了不支持的操作,可能遇到 TypeError;访问不存在的列表位置,可能遇到 IndexError;打开不存在的文件,则可能遇到 FileNotFoundError。

为什么 Python 要设计这么多异常类型?遇到不同的问题时,我们又应该选择哪一种异常进行处理?

这篇文章就从实际问题出发,逐步认识 Python 的常见异常类型、异常继承关系,以及异常处理时的选择思路。

为什么需要不同的异常类型

先看一个简单的例子。

假设我们编写了一个服务器监控程序,需要将用户输入的 CPU 使用率转换为整数。

创建或打开 cpu_check.py,写入下面的代码:

1
2
3
cpu_usage = int(input("请输入 CPU 使用率:"))

print(f"当前 CPU 使用率为:{cpu_usage}%")

运行程序:

1
python3 cpu_check.py

输入 abc,程序会因为无法将字符串转换为整数而产生 ValueError 异常。

如果我们尝试把字符串和整数直接相加:

1
2
3
result = "CPU 使用率:" + 75

print(result)

程序则会产生 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
2
3
cpu_usage = int("abc")

print(cpu_usage)

运行后,Python 会报告 ValueError。

原因是 int() 可以把 "75" 这样的数字字符串转换为整数,但无法把 "abc" 转换为整数。

1
print(int("75"))

输出:

1
75

而下面的代码无法正常执行:

1
print(int("abc"))

因此,当数据的值不符合某个操作的要求时,就可能出现 ValueError。

需要注意,ValueError 并不只用于数字转换。例如,某些函数接收了类型正确、但取值不合法的参数时,也可能产生这种异常。

TypeError:类型不合适

再看下面的代码:

1
2
3
4
5
cpu_usage = 75

result = "CPU 使用率:" + cpu_usage

print(result)

运行后,会产生 TypeError。

为什么?

因为 "CPU 使用率:" 是字符串,而 cpu_usage 是整数。Python 不允许直接使用 + 将这两种数据相加。

如果我们希望把整数转换为字符串,可以使用 str():

1
2
3
4
5
cpu_usage = 75

result = "CPU 使用率:" + str(cpu_usage)

print(result)

输出:

1
CPU 使用率:75

这里,str(cpu_usage) 将整数转换成字符串,两个字符串就可以使用 + 拼接了。

这个例子说明,TypeError 通常意味着操作或函数调用使用了不合适的数据类型。

IndexError:列表位置不存在

假设我们需要保存几台服务器的名称:

1
2
3
4
5
servers = ["web01", "web02", "web03"]

print(servers[0])
print(servers[1])
print(servers[2])

输出:

1
2
3
web01
web02
web03

这里的 servers 是一个列表,可以把它理解为一个按照顺序存放多个数据的容器。

Python 列表的位置编号从 0 开始,而不是从 1 开始。

因此,这个列表的有效位置是 0、1 和 2。

如果继续访问位置 3:

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

print(servers[3])

程序就会产生 IndexError,因为列表中没有这个位置。

KeyError:字典中的键不存在

在运维工作中,我们可能需要记录服务器名称和 IP 地址之间的对应关系。

例如:

1
2
3
4
5
6
7
servers = {
"web01": "192.168.1.10",
"web02": "192.168.1.11",
"web03": "192.168.1.12"
}

print(servers["web01"])

输出:

1
192.168.1.10

这里使用了 Python 字典。字典可以通过一个键查找对应的值,例如通过服务器名称查找 IP 地址。

如果访问不存在的键:

1
2
3
4
5
6
7
servers = {
"web01": "192.168.1.10",
"web02": "192.168.1.11",
"web03": "192.168.1.12"
}

print(servers["web04"])

程序就会产生 KeyError,因为字典中不存在 "web04" 这个键。

如果只是希望查询某台服务器的 IP 地址,并且允许服务器不存在,可以使用 get():

1
2
3
4
5
6
7
servers = {
"web01": "192.168.1.10",
"web02": "192.168.1.11",
"web03": "192.168.1.12"
}

print(servers.get("web04", "未找到服务器"))

输出:

1
未找到服务器

get() 的第二个参数表示:如果找不到指定的键,就返回这个默认值。

因此,查询字典时并不一定要通过捕获 KeyError 来解决问题。根据实际需求选择合适的方法,往往更加简单。

FileNotFoundError:文件不存在

假设我们需要读取一份服务器清单:

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

print(content)

这段代码尝试打开当前工作目录中的 servers.txt 文件。

如果文件不存在,Python 通常会产生 FileNotFoundError。

这里的 with open() 用于打开文件,并在离开代码块时自动关闭文件。暂时不必深入理解它的全部语法,只需要知道 open() 会尝试访问指定的文件即可。

文件不存在只是可能的原因之一。如果文件存在,但当前用户没有足够的访问权限,也可能遇到其他异常,例如 PermissionError。

用 try 和 except 处理不同的异常

前面我们认识了 ValueError 和 TypeError 等异常类型。现在的问题是:如果程序遇到不同的问题,能不能分别给出不同的提示?

当然可以。我们可以在同一个 try 代码块后面编写多个 except 子句,让 Python 根据实际发生的异常,选择对应的处理逻辑。

先看下面的例子。将代码保存到 cpu_check.py:

1
2
3
4
5
6
7
8
9
10
11
try:
cpu_usage = int(input("请输入 CPU 使用率:"))

result = "当前 CPU 使用率为:" + cpu_usage
print(result)

except ValueError:
print("输入格式错误,请输入一个整数!")

except TypeError:
print("数据类型不符合要求,无法完成字符串拼接!")

运行程序:

1
python3 cpu_check.py

测试 ValueError

输入 abc:

1
2
请输入 CPU 使用率:abc
输入格式错误,请输入一个整数!

程序执行到 int(input(...)) 时,发现 abc 无法转换为整数,于是产生 ValueError。

Python 找到匹配的 except ValueError,执行对应的错误处理代码。

由于异常发生在 try 代码块内部,后面的字符串拼接语句不会执行。

测试 TypeError

再次运行:

1
python3 cpu_check.py

输入 75:

1
2
请输入 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
2
3
4
5
6
try:
with open("servers.txt", "r", encoding="utf-8") as file:
content = file.read()

except FileNotFoundError:
print("服务器清单文件不存在!")

也可以捕获更广泛的 OSError:

1
2
3
4
5
6
try:
with open("servers.txt", "r", encoding="utf-8") as file:
content = file.read()

except OSError:
print("读取文件时发生操作系统相关错误!")

第二种写法可以捕获 FileNotFoundError,也可以捕获其他属于 OSError 的异常,例如 PermissionError。

不过,捕获范围越宽,并不代表代码就越好。如果不同异常需要不同的处理方式,应该优先使用更具体的异常类型;如果多种相关异常确实可以使用同一种处理方式,再考虑捕获它们共同的父类。

使用 issubclass() 验证异常的继承关系

我们还可以使用 Python 内置函数 issubclass() 来检查两个类之间是否存在继承关系。

例如:

1
2
3
4
print(issubclass(FileNotFoundError, OSError))
print(issubclass(IndexError, LookupError))
print(issubclass(KeyError, LookupError))
print(issubclass(ValueError, LookupError))

将代码保存到 cpu_check.py,然后运行:

1
python3 cpu_check.py

输出:

1
2
3
4
True
True
True
False

为什么会出现这样的结果?

  • FileNotFoundError 是 OSError 的子类,因此结果为 True。
  • IndexError 是 LookupError 的子类,因此结果为 True。
  • KeyError 也是 LookupError 的子类,因此结果为 True。
  • ValueError 不属于 LookupError 的继承体系,因此结果为 False。

这里的 issubclass(子类, 父类) 用于判断前面的类是否继承自后面的类,或者是否通过其他继承路径属于该类的子类。

注意,issubclass() 判断的是类之间的关系,而不是某个变量当前保存的数据属于什么类型。后者可以使用之前学过的 isinstance()。

写在最后

通过这篇文章,我们认识了 ValueError、TypeError、IndexError、KeyError 和 FileNotFoundError 等常见异常,了解了它们分别适用于哪些场景。

我们还通过异常选择流程图,梳理了从判断问题原因到选择异常类型、再到决定如何处理的基本思路。

此外,我们学习了 Python 异常的继承关系,知道了异常不仅有具体类型,还可以通过父类组织成层次结构。利用这种关系,程序既可以捕获具体异常,也可以根据需要捕获一组相关异常。

实际编程时,不必死记所有异常类型。更重要的是,遇到问题时能够读懂异常名称和报错信息,判断错误发生的原因,再选择合适的处理方式。

下一篇,我们将学习如何使用 raise 主动引发异常,让程序在发现不符合业务规则的情况时,主动报告问题,而不只是等待 Python 自动产生异常。