6.8 异常体系与内置异常类型
程序运行中难免遇到各种意外情况,Python 用异常来表示这些情况。异常是一个对象,携带错误类型和相关信息,沿调用栈向上传播,直到被某个 try-except 捕获或导致程序终止。理解异常的层次结构,才能写出精确的异常处理代码。捕获过于宽泛会掩盖真正的错误,捕获过于狭窄又会让本该处理的异常逃逸。本节将讲解 Python 异常体系的层次结构、常见的内置异常类型及其继承关系,以及异常对象的 args 属性。任务管理器在文件读写、数据解析等操作中会遇到多种异常,了解它们的分类有助于分别处理。
6.8.1 BaseException 异常层次结构的根
Python 所有异常的根类是 BaseException。它定义了异常对象的基本行为,包括 args 属性、__str__ 方法和异常链相关属性。所有内置异常和用户自定义异常都直接或间接继承自 BaseException。
异常层次的最顶层分为三个直接子类:Exception、SystemExit、KeyboardInterrupt、GeneratorExit。这种划分把普通程序错误与系统级中断分开,避免用 except Exception: 捕获到不该捕获的中断。
# 查看异常的继承链
print(BaseException.__subclasses__())
# [<class 'BaseExceptionGroup'>, <class 'Exception'>, <class 'KeyboardInterrupt'>,
# <class 'GeneratorExit'>, <class 'SystemExit'>]SystemExit 由 sys.exit() 引发,表示请求程序退出。KeyboardInterrupt 由用户按 Ctrl+C 引发,表示请求中断程序。GeneratorExit 在生成器关闭时引发。这三个异常都不继承 Exception,因此 except Exception: 不会捕获它们,这是有意的设计,保证用户中断和退出请求能正常传达。
6.8.2 Exception 基类
Exception 是所有普通程序异常的基类。绝大多数内置异常和所有自定义异常都继承自 Exception。捕获异常时应当使用 except Exception: 而不是裸 except:,这样既能捕获几乎所有程序错误,又不会吞掉 KeyboardInterrupt 和 SystemExit。
# 推荐的兜底捕获
try:
# 某些可能出错的操作
result = 10 / 0
except Exception as e:
print(f"捕获到异常:{type(e).__name__}: {e}")
# 捕获到异常:ZeroDivisionError: division by zeroException 的直接子类包括 ArithmeticError、LookupError、ValueError、TypeError、OSError 等,每个子类下面又有更具体的异常类型。这种层次结构让异常处理可以按需要的粒度进行:捕获父类处理整类异常,捕获子类处理特定异常。
6.8.3 数值与算术异常
ArithmeticError 基类
ArithmeticError 是所有算术异常的基类,它的子类包括 ZeroDivisionError、OverflowError、FloatingPointError。
ZeroDivisionError 除零错误
除法或取模运算的除数为零时引发:
try:
result = 10 / 0
except ZeroDivisionError as e:
print(e) # division by zeroOverflowError 数值溢出
数值运算结果超出当前类型的表示范围时引发。Python 的整数是任意精度的,不会溢出;浮点数运算可能溢出:
import math
try:
result = math.exp(1000) # 超过浮点数范围
except OverflowError as e:
print(e) # math range errorFloatingPointError 在浮点操作失败时引发,实际很少触发,因为 Python 默认使用 IEEE 754 浮点标准,异常情况由特殊值(如 inf、nan)表示而非抛出异常。
6.8.4 查找异常
LookupError 基类
LookupError 是所有查找失败的基类,子类包括 IndexError 和 KeyError。
IndexError 索引越界
序列索引超出范围时引发:
data = [1, 2, 3]
try:
print(data[5])
except IndexError as e:
print(e) # list index out of rangeKeyError 键不存在
字典查找不存在的键时引发:
config = {"mode": "auto"}
try:
print(config["timeout"])
except KeyError as e:
print(f"缺少配置项:{e}") # 缺少配置项:'timeout'KeyError 的异常消息就是缺失的键本身,不像其他异常那样是一段描述文字。任务管理器在解析配置文件时,如果某个必需字段缺失,会触发 KeyError。
6.8.5 类型与值异常
TypeError 类型错误
操作或函数应用于不适当类型时引发。比如对字符串和数字做加法:
try:
result = "数量:" + 5
except TypeError as e:
print(e) # can only concatenate str (not "int") to strTypeError 通常意味着代码逻辑有误,传入了错误类型的数据。修正方法是显式做类型转换:"数量:" + str(5)。
ValueError 值错误
操作数类型正确但值不合适时引发。最常见的是 int() 转换非数字字符串:
try:
number = int("abc")
except ValueError as e:
print(e) # invalid literal for int() with base 10: 'abc'TypeError 和 ValueError 的区别在于:TypeError 是类型不对,ValueError 是类型对但值不合法。int("abc") 中参数是字符串,类型正确,但字符串内容不是有效数字,所以是 ValueError。int([1,2]) 中参数是列表,类型就不对,所以是 TypeError。
任务管理器在解析用户输入的任务优先级时,如果用户输入非数字,会触发 ValueError,这是需要处理的典型场景。
6.8.6 Unicode 异常
UnicodeError 是 ValueError 的子类,专门表示 Unicode 编码解码错误。它又有三个子类:UnicodeEncodeError(编码失败)、UnicodeDecodeError(解码失败)、UnicodeTranslateError(转换失败)。
# 编码错误:把无法编码的字符写入 ASCII 文件
try:
"你好".encode("ascii")
except UnicodeEncodeError as e:
print(e) # 'ascii' codec can't encode characters...
# 解码错误:用错误编码读取文件
try:
b"\xff\xfe".decode("utf-8")
except UnicodeDecodeError as e:
print(e) # 'utf-8' codec can't decode byte 0xff...文件操作中编码不匹配是触发 UnicodeDecodeError 的常见原因。第 12 章会详细讨论文件操作中的编码异常场景。
6.8.7 运行时异常
RuntimeError 基类
RuntimeError 是一个比较通用的异常,表示运行时出现的、不属于其他类别的错误。它的子类包括 RecursionError 和 NotImplementedError。
RecursionError 递归深度超限
递归调用超过 Python 的递归深度限制时引发:
def infinite_recursion():
return infinite_recursion()
try:
infinite_recursion()
except RecursionError as e:
print(e) # maximum recursion depth exceeded默认递归深度限制通常是 1000,可以通过 sys.setrecursionlimit() 调整,但不建议设置过大,可能导致解释器崩溃。
NotImplementedError 未实现
抽象方法或占位函数被调用时引发,表示功能尚未实现:
class TaskStorage:
def save(self, tasks):
raise NotImplementedError("子类必须实现 save 方法")
storage = TaskStorage()
try:
storage.save([])
except NotImplementedError as e:
print(e) # 子类必须实现 save 方法6.8.8 操作系统异常 OSError
OSError 是所有操作系统相关错误的基类,Python 3.3 起许多原来独立的异常类(如 IOError、EnvironmentError、WindowsError)都合并为 OSError 的别名或子类。文件操作中的绝大多数错误都属于 OSError 的子类。
FileNotFoundError 文件不存在
以读取模式打开不存在的文件时引发:
try:
with open("missing.txt", "r", encoding="utf-8") as f:
content = f.read()
except FileNotFoundError as e:
print(e) # [Errno 2] No such file or directory: 'missing.txt'PermissionError 权限不足
没有权限访问文件或目录时引发:
try:
with open("/etc/shadow", "r") as f:
content = f.read()
except PermissionError as e:
print(e) # [Errno 13] Permission denied: '/etc/shadow'IsADirectoryError 和 NotADirectoryError
IsADirectoryError 在对目录执行文件操作时引发,NotADirectoryError 在对文件执行目录操作时引发:
import os
try:
with open("data", "r") as f: # data 是目录
content = f.read()
except IsADirectoryError as e:
print(e) # [Errno 21] Is a directory: 'data'FileExistsError 文件已存在
用独占创建模式 'x' 打开已存在的文件时引发:
try:
with open("tasks.txt", "x", encoding="utf-8") as f:
f.write("新文件")
except FileExistsError as e:
print(e) # [Errno 17] File exists: 'tasks.txt'这些 OSError 的子类构成了文件操作异常处理的核心。第 12 章会系统讲解文件操作中各种异常场景的处理策略。
6.8.9 其他常见异常类型
AttributeError 属性错误
访问对象不存在的属性时引发:
tasks = []
try:
tasks.append_item("新任务") # 方法名写错
except AttributeError as e:
print(e) # 'list' object has no attribute 'append_item'NameError 名称未定义
使用未定义的变量名时引发:
try:
print(undefined_var)
except NameError as e:
print(e) # name 'undefined_var' is not definedStopIteration 迭代结束
迭代器没有更多元素时引发,通常由 next() 或 for 循环内部处理,不需要手动捕获:
it = iter([1])
next(it) # 1
try:
next(it) # 没有更多元素
except StopIteration:
print("迭代结束")AssertionError 断言失败
assert 语句条件为 False 时引发,第 11 章详细讲解。
MemoryError 内存不足
程序请求的内存超过可用内存时引发。捕获 MemoryError 通常意义不大,因为此时程序可能已经无法正常工作。
SyntaxError 语法错误
代码语法不正确时在编译阶段引发,不是运行时异常:
try:
eval("1 + ") # 语法错误的表达式
except SyntaxError as e:
print(e) # unexpected EOF while parsing6.8.10 异常的继承关系与匹配规则
异常捕获遵循继承关系匹配原则。except 子句指定的异常类型,能捕获该类型及其所有子类的异常。比如 except OSError: 能捕获 FileNotFoundError、PermissionError 等所有 OSError 的子类。
try:
open("missing.txt", "r")
except OSError as e:
# FileNotFoundError 是 OSError 的子类,这里也能捕获
print(f"操作系统错误:{type(e).__name__}: {e}")
# 操作系统错误:FileNotFoundError: [Errno 2] No such file or directory: 'missing.txt'这一特性在异常处理中既有用又危险。有用在于可以用一个 except 处理一类异常,危险在于可能意外捕获到本该单独处理的子类异常。编写多个 except 子句时,应当把更具体的子类放在前面,更通用的父类放在后面,否则子类 except 永远不会执行。
# 正确顺序:具体在前,通用在后
try:
open("missing.txt", "r")
except FileNotFoundError:
print("文件不存在")
except PermissionError:
print("权限不足")
except OSError:
print("其他操作系统错误")6.8.11 异常对象的 args 属性
异常对象通过 args 属性保存创建时传入的参数。对于 raise ValueError("消息"),args 是 ('消息',)。对于不传参数的异常,args 是空元组 ()。
try:
raise ValueError("无效的优先级", 99)
except ValueError as e:
print(e.args) # ('无效的优先级', 99)
print(e.args[0]) # 无效的优先级
print(e.args[1]) # 99
print(str(e)) # ('无效的优先级', 99)str(e) 就是把 args 转成字符串。单个参数时显示参数本身,多个参数时显示元组形式。args 在日志记录和调试时很有用,可以访问异常的原始参数而不仅是字符串表示。
异常对象还有 __cause__、__context__、__suppress_context__ 三个属性与异常链相关,第 10 章讲解 raise ... from ... 时会详细介绍。
6.8.12 异常类型的查询辅助
Python 提供了几种查询异常类型关系的方式。issubclass(cls, parent) 判断一个类是否是另一个类的子类,type(e).__mro__ 查看异常的方法解析顺序(即继承链)。
print(issubclass(FileNotFoundError, OSError)) # True
print(issubclass(FileNotFoundError, Exception)) # True
print(issubclass(FileNotFoundError, ValueError)) # False
# 查看继承链
print(FileNotFoundError.__mro__)
# (<class 'FileNotFoundError'>, <class 'OSError'>, <class 'Exception'>,
# <class 'BaseException'>, <class 'object'>)在不确定两个异常类的继承关系时,issubclass() 是可靠的查询方式。__mro__ 展示完整的继承链,有助于理解异常在层次结构中的位置。
练习题
- 解释为什么应当用
except Exception:而不是裸except:作为兜底捕获。如果用裸except:捕获,会出现什么问题?
参考答案
裸 except: 会捕获所有继承自 BaseException 的异常,包括 KeyboardInterrupt 和 SystemExit。这两个异常分别表示用户按 Ctrl+C 中断程序和 sys.exit() 请求退出,它们属于控制信号,不代表程序错误。
如果用裸 except: 捕获,按 Ctrl+C 无法中断程序,sys.exit() 无法正常退出,程序会继续运行,这对用户和运维都是灾难性的。
except Exception: 只捕获 Exception 及其子类,而 KeyboardInterrupt 和 SystemExit 不继承 Exception,所以不会被捕获,能正常传达中断和退出请求。因此兜底捕获应当用 except Exception:。
- 以下操作分别会触发什么异常?写出异常类型名。
int("abc")
[1, 2, 3][5]
{"a": 1}["b"]
"hello".append("!")
1 / 0
open("not_exist.txt", "r")参考答案
int("abc") # ValueError,类型正确但值无效
[1, 2, 3][5] # IndexError,序列索引越界
{"a": 1}["b"] # KeyError,字典键不存在
"hello".append("!") # AttributeError,字符串没有 append 方法
1 / 0 # ZeroDivisionError,除以零
open("not_exist.txt", "r") # FileNotFoundError,文件不存在这些是最常见的内置异常类型。ValueError 和 TypeError 的区别在于参数类型是否正确:int("abc") 中参数是字符串,类型正确,但内容无法转换,所以是 ValueError。如果传 int([1,2]),参数类型不对,会是 TypeError。
- 以下 except 子句的顺序有什么问题?请改正并说明原因。
try:
open("tasks.txt", "r")
except OSError:
print("操作系统错误")
except FileNotFoundError:
print("文件不存在")参考答案
问题在于 FileNotFoundError 是 OSError 的子类,按从上到下的顺序匹配,except OSError: 会先匹配到 FileNotFoundError,导致 except FileNotFoundError: 永远不会执行。Python 会给出语法警告。
改正方法是把更具体的子类放在前面,更通用的父类放在后面:
try:
open("tasks.txt", "r")
except FileNotFoundError:
print("文件不存在")
except PermissionError:
print("权限不足")
except OSError:
print("其他操作系统错误")这样 FileNotFoundError 会被第一个 except 捕获,其他 OSError 子类按顺序匹配,最后 except OSError: 兜底捕获未被前面覆盖的 OSError 子类。
- 给定以下代码,写出捕获异常后
e.args的值。
try:
raise ValueError("优先级超出范围", 99, "max=10")
except ValueError as e:
print(e.args)参考答案
e.args 的值是 ('优先级超出范围', 99, 'max=10'),一个包含三个元素的元组。
args 属性保存创建异常时传入的所有参数。raise ValueError("优先级超出范围", 99, "max=10") 传入了三个参数,它们被原样保存到 args 元组中。可以通过 e.args[0]、e.args[1]、e.args[2] 分别访问。str(e) 会把整个元组转为字符串显示为 ('优先级超出范围', 99, 'max=10')。
在任务管理器中,可以用多个参数传递结构化的错误信息,比如错误码、字段名、允许范围等,方便调用方程序化处理,而不只是依赖字符串消息。
常见错误
错误 1 · SyntaxWarning: catching a class that does not inherit from BaseException 或子类 except 永不执行
原因:多个 except 子句的顺序写反,把父类异常写在子类前面。由于 except 按从上到下匹配,父类会先匹配到子类异常,导致后面的子类 except 永远无法执行。Python 会对部分情况给出警告。
解决:把更具体的子类异常放在前面,更通用的父类异常放在后面。例如先 except FileNotFoundError:,再 except OSError:。不确定继承关系时用 issubclass() 查询。
错误 2 · Ctrl+C 无法中断程序,sys.exit() 无法退出
原因:使用了裸 except: 作为兜底捕获,它会捕获所有 BaseException 的子类,包括 KeyboardInterrupt 和 SystemExit。这两个异常是控制信号而非程序错误,被吞掉后用户无法中断程序,退出请求也被屏蔽。
解决:兜底捕获用 except Exception: 而非裸 except:。Exception 不包含 KeyboardInterrupt 和 SystemExit,二者能正常传达。确实需要捕获所有异常时,应分别显式处理 KeyboardInterrupt 和 SystemExit。
错误 3 · TypeError 与 ValueError 混淆导致捕获错误
原因:把类型不匹配的错误误判为值错误,或反之。int("abc") 是 ValueError(类型对但值无效),int([1,2]) 是 TypeError(类型不对)。捕获了错误的异常类型会导致另一个异常逃逸。
解决:区分异常类型的依据是参数类型是否正确。类型正确但值不合法是 ValueError,类型本身不对是 TypeError。不确定时用 except (TypeError, ValueError): 同时捕获两种,或在 except 中用 type(e).__name__ 打印实际异常类型。