理解一个问题,就是用一百种方式来表达它。
🛡️ 异常基础
异常(exception)是程序运行过程中发生的错误信号。当 Python 无法继续执行某段代码时,就会抛出(raise)一个异常对象;如果不处理这个异常,程序就会终止并打印错误信息。理解异常的工作机制是编写健壮代码的第一步——它使得预见错误、优雅恢复、给出有意义的反馈成为可能,而不是让程序崩溃。
📌 本节要点
- 异常是特殊的对象,描述程序运行中遇到的问题
- 所有异常派生自
BaseException,普通错误继承Exception - 异常层次结构允许按"分组"捕获(如
LookupError包含KeyError和IndexError) - 常见内置异常:
ValueError、TypeError、KeyError、IndexError、FileNotFoundError等 - 阅读 Traceback:先看最后一行异常类型,再从底部往上追调用栈
- 异常即对象,可检查属性、附加 note、重新抛出
raise主动抛异常时,务必附带清晰的信息
什么是异常
异常本质上是一种特殊的对象,它描述了程序运行中遇到的问题。当错误发生时,Python 会创建一个异常对象并中断当前执行流,沿着调用栈向上传递,直到被某个 try 语句捕获处理;如果一直没被捕获,程序就会终止并打印 Traceback。
# 一个未捕获的异常会让程序崩溃
print(10 / 0)
# ZeroDivisionError: division by zero
执行上面的代码,会看到类似这样的错误输出:
Traceback (most recent call last):
File "example.py", line 1, in <module>
print(10 / 0)
~~~^^^
ZeroDivisionError: division by zero
异常不是设计缺陷,而是 Python 与开发者交流错误的方式。一个好的程序应当主动抛出异常来标识异常情况,并合理处理那些预期可能发生的异常。完全消除异常既不可能也不必要。
异常 vs 错误返回码
一些语言(如 C、Go)用返回值表示错误,调用者必须检查每个返回值。Python 选择用异常来表达错误,这样做的好处是:
- 正常逻辑与错误处理分离:核心代码不会被错误检查淹没
- 错误不会"被忽略":忘记处理异常会导致程序崩溃,而不是悄悄返回错误结果
- 错误向上传递:异常会自动沿着调用栈向上传播,不需要逐层手动转发
# 返回码风格(Go/C 风格,不 Pythonic)
def divide_code(a, b):
if b == 0:
return None, "division by zero"
return a / b, None
result, err = divide_code(10, 0)
if err is not None:
print(f"错误:{err}")
else:
print(result)
# 异常风格(Pythonic)
def divide(a, b):
if b == 0:
raise ZeroDivisionError("division by zero")
return a / b
try:
print(divide(10, 0))
except ZeroDivisionError as e:
print(f"错误:{e}")
异常层次结构
Python 的所有异常都从一个根类 BaseException 派生。绝大多数用户遇到的异常都继承自 Exception,这是"普通错误"的基类。
Python 异常继承层级:
BaseException
├── SystemExit # sys.exit() 触发
├── KeyboardInterrupt # Ctrl+C 中断
├── GeneratorExit # 生成器被关闭
└── Exception # 所有普通异常的基类
├── StopIteration # 迭代器耗尽
├── StopAsyncIteration # 异步迭代器耗尽
├── ArithmeticError
│ ├── ZeroDivisionError
│ ├── OverflowError
│ └── FloatingPointError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── AttributeError
├── OSError
│ ├── FileNotFoundError
│ ├── FileExistsError
│ ├── PermissionError
│ ├── IsADirectoryError
│ └── TimeoutError
├── TypeError
├── ValueError
│ └── UnicodeError
├── NameError
├── RuntimeError
│ └── NotImplementedError
├── Warning # 警告的基类
│ ├── DeprecationWarning
│ ├── UserWarning
│ └── ...
└── ... 还有更多
BaseException 与 Exception
# 异常本身也是对象,可以查看其继承关系
print(ZeroDivisionError.__mro__)
# 输出:
# (<class 'ZeroDivisionError'>, <class 'ArithmeticError'>,
# <class 'Exception'>, <class 'BaseException'>, <class 'object'>)
print(isinstance(ZeroDivisionError(), Exception)) # 输出:True
print(isinstance(ZeroDivisionError(), BaseException)) # 输出:True
print(isinstance(Exception(), BaseException)) # 输出:True
BaseException 包含了 KeyboardInterrupt(用户按 Ctrl+C)和 SystemExit(程序主动退出)等"非错误"信号。直接捕获 BaseException 会把这些信号也吞掉,导致无法中断或正常退出程序。日常开发中只捕获 Exception 及其子类,让 Ctrl+C 等信号能正常穿透。
异常的派生关系用于分组
异常的层次结构使得既能精确捕获某个具体异常,也能捕获整组异常:
def safe_get(mapping, key, default=None):
try:
return mapping[key]
except KeyError: # 精确捕获 KeyError
return default
except LookupError: # 兜底捕获所有查找类错误(更宽泛)
return default
print(safe_get({"a": 1}, "b", "默认值")) # 输出:默认值
常见内置异常
Python 内置了大量异常类型,下面是最常用的几个。
ValueError
值类型正确,但内容不合法时抛出。
# 把字符串转成整数,但内容不是数字
n = int("abc")
# ValueError: invalid literal for int() with base 10: 'abc'
# 从列表中移除不存在的元素
nums = [1, 2, 3]
nums.remove(5)
# ValueError: list.remove(x): x not in list
TypeError
类型不匹配,对不支持该操作的对象执行了某种运算。
# 数字与字符串相加
result = 1 + "2"
# TypeError: unsupported operand type(s) for +: 'int' and 'str'
# 对不可迭代对象做迭代
for x in 42:
pass
# TypeError: 'int' object is not iterable
KeyError
字典中找不到指定键。
user = {"name": "Alice", "age": 30}
print(user["email"])
# KeyError: 'email'
# 用 .get() 或 in 检查可避免
print(user.get("email", "未填写")) # 输出:未填写
IndexError
索引超出序列范围。
nums = [10, 20, 30]
print(nums[5])
# IndexError: list index out of range
FileNotFoundError
文件不存在(属于 OSError 的子类,Python 3.3+)。
with open("不存在的文件.txt") as f:
content = f.read()
# FileNotFoundError: [Errno 2] No such file or directory: '不存在的文件.txt'
ZeroDivisionError
除以零或对零取模。
print(5 / 0) # ZeroDivisionError: division by zero
print(5 % 0) # ZeroDivisionError: integer division or modulo by zero
AttributeError
访问对象不存在的属性或方法。
name = "Alice"
print(name.uppercase())
# AttributeError: 'str' object has no attribute 'uppercase'
# Did you mean: 'upper'?
AttributeError 和 NameError 会自动给出"你是不是想用……"的建议(如 Did you mean: 'upper'?),这对快速定位拼写错误非常友好。
NameError
访问未定义的变量名。
print(undefined_var)
# NameError: name 'undefined_var' is not defined. Did you mean: 'undefined'?
RuntimeError
运行时出现的一般性错误,通常不属于其他更具体的类别时使用。其子类 NotImplementedError 表示某功能尚未实现。
class Animal:
def speak(self):
raise NotImplementedError("子类必须实现 speak()")
Animal().speak()
# NotImplementedError: 子类必须实现 speak()
一览表
| 异常名 | 触发场景 |
|---|---|
ValueError | 值类型正确但内容不合法 |
TypeError | 类型不匹配 |
KeyError | 字典键不存在 |
IndexError | 索引越界 |
FileNotFoundError | 文件不存在 |
PermissionError | 权限不足 |
ZeroDivisionError | 除零 |
AttributeError | 属性不存在 |
NameError | 变量名未定义 |
ImportError | 模块导入失败 |
StopIteration | 迭代器耗尽 |
RuntimeError | 一般运行时错误 |
NotImplementedError | 功能未实现 |
OSError | 系统相关错误(基类) |
阅读 Traceback
Traceback(回溯)是程序崩溃时打印的错误堆栈。学会阅读它,是排查问题的核心技能。
Traceback 的结构
Traceback (most recent call last): ← 1. 错误类型标记
File "main.py", line 10, in <module> ← 2. 调用栈(最外层→最内层)
result = process(data)
~~~~~~~^^^^^^^ ← 3. 出错的代码位置(3.11+ 高亮)
File "main.py", line 6, in process ← 4. 更深一层的调用
return data["value"] / 0
~~~~^^^^^^^^~~
ZeroDivisionError: division by zero ← 5. 最终异常类型与信息
阅读顺序建议:
- 从最后一行看起:异常类型 + 错误信息,立刻知道发生了什么
- 从底部往上读调用栈:每一段都标明"哪个文件、哪一行、哪个函数"出错
- 关注最顶部的调用:那是离错误源头最近的位置
Python 3.11 起大幅改进了错误信息:会用 ^ 符号精确高亮出错的表达式片段,而不是整行。例如:
File "x.py", line 2
x['a']['b'] = 1
^^^^^^^^
TypeError: 'int' object is not subscriptable
这样就能一眼定位到是 x['a'] 这部分出了问题。
一个完整的示例
def load_config(path):
with open(path) as f: # ① 这里可能抛 FileNotFoundError
lines = f.readlines()
return dict(line.split("=", 1) for line in lines)
def main():
config = load_config("missing.conf") # ② 这里调用
print(config["debug"]) # ③ 可能抛 KeyError
main()
如果 missing.conf 不存在,运行后会看到:
Traceback (most recent call last):
File "main.py", line 9, in <module>
main()
File "main.py", line 6, in main
config = load_config("missing.conf")
~~~~~~~~~^^^^^^^^^^^^^^^^^^
File "main.py", line 2, in load_config
with open(path) as f:
~~~~^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'missing.conf'
从下往上读:最后一行提示"找不到文件";再往上找到出错代码位置在 load_config 第 2 行的 open(path);再往上看,调用来源是 main 函数第 6 行。
异常即对象
异常在 Python 中就是普通的类实例对象。这意味着我们可以:
- 检查异常的属性:错误信息、错误码等
- 捕获后再次抛出:保留原始上下文
- 创建自定义异常类:继承
Exception表达业务错误 - 用 isinstance 判断异常类型
异常的属性和方法
exc = ValueError("无效的输入:负数")
print(type(exc)) # 输出:<class 'ValueError'>
print(str(exc)) # 输出:无效的输入:负数
print(exc.args) # 输出:('无效的输入:负数',)
print(repr(exc)) # 输出:ValueError('无效的输入:负数')
# 异常对象支持解包
msg, = exc.args
print(msg) # 输出:无效的输入:负数
OSError 的额外属性
OSError 及其子类(FileNotFoundError、PermissionError 等)额外携带 errno 和 strerror 属性:
import errno
try:
with open("/root/protected.txt") as f:
f.read()
except OSError as e:
print(f"错误码 errno: {e.errno}") # 13 (EACCES)
print(f"错误信息 strerror: {e.strerror}") # Permission denied
print(f"文件名 filename: {e.filename}") # /root/protected.txt
# 对照常见错误码
if e.errno == errno.ENOENT:
print("文件不存在")
elif e.errno == errno.EACCES:
print("权限不足")
捕获后保留异常对象
使用 as 把异常对象绑定到变量,便于进一步检查或记录日志:
def parse_age(text: str) -> int:
try:
age = int(text)
if age < 0 or age > 150:
raise ValueError(f"年龄 {age} 不在合法范围内")
return age
except ValueError as exc:
# exc 是异常对象,可以记录或重新抛出
print(f"解析失败:{exc}(参数:{exc.args})")
return -1
print(parse_age("25")) # 输出:25
print(parse_age("abc")) # 输出:解析失败:invalid literal...(参数:...) / -1
print(parse_age("200")) # 输出:解析失败:年龄 200 不在合法范围内 / -1
Python 3.11+ 给异常增加了 add_note() 方法,可以在异常对象上附加额外说明信息,便于调试时记录更多上下文:
try:
data = {"x": 1}
value = data["y"]
except KeyError as exc:
exc.add_note(f"当前字典内容:{data}")
exc.add_note("检查键名是否拼写正确")
raise # 重新抛出,note 会一并打印
打印时这些 note 会出现在 Traceback 之后,对于复杂场景的调试极其有用。
异常的抛出
使用 raise 语句主动抛出异常。可以抛出内置异常,也可以抛出自定义异常类。
def withdraw(balance: float, amount: float) -> float:
if amount <= 0:
raise ValueError("取款金额必须为正数")
if amount > balance:
raise ValueError(f"余额不足:当前 {balance},需取 {amount}")
return balance - amount
# 抛出时可以只给类名,也可以给实例
raise ValueError("错误描述")
raise ValueError # 等价于 raise ValueError(),但通常应附带说明
不要写裸的 raise ValueError()——没有任何描述信息会让排查变得困难。永远提供清晰、可操作的错误信息,说明发生了什么以及期望是什么。
重新抛出当前异常
在 except 块中可以使用裸 raise 重新抛出当前捕获的异常,保留完整的原始上下文:
def process(data: dict) -> int:
try:
return int(data["value"])
except KeyError:
print("警告:缺少 value 字段")
raise # 重新抛出 KeyError,让上层处理
try:
process({})
except KeyError:
print("上层捕获到 KeyError")
# 输出:
# 警告:缺少 value 字段
# 上层捕获到 KeyError
实战:识别常见异常
下面这段代码故意触发了多种常见异常,让我们尝试阅读 Traceback 并识别每种异常的类型与原因:
def buggy_pipeline(data: dict) -> str:
"""演示多种异常的混合处理"""
# 1. KeyError:缺少键
try:
name = data["name"]
except KeyError:
name = "匿名"
# 2. ValueError:类型转换失败
try:
age = int(data.get("age", "未知"))
except (ValueError, TypeError) as exc:
print(f" 年龄解析失败:{exc}")
age = 0
# 3. TypeError:类型不匹配的运算
try:
score = data.get("score", 0)
normalized = score / 100 # 若 score 是 str 会 TypeError
except TypeError as exc:
print(f" 分数计算失败:{exc}")
normalized = 0.0
# 4. IndexError:索引越界
try:
first_tag = data["tags"][0]
except (KeyError, IndexError) as exc:
print(f" 获取标签失败:{exc}")
first_tag = None
return f"{name}({age} 岁),归一化分数:{normalized:.2f},标签:{first_tag}"
# 故意传入不规范的数据触发各种异常
samples = [
{"name": "Alice", "age": "30", "score": 85, "tags": ["vip"]},
{"age": "abc", "score": "九十分"}, # 缺 name、age 非数字、score 是字符串
{}, # 全空字典
{"name": "Bob", "age": 25, "score": 90, "tags": []}, # tags 为空列表
]
for i, sample in enumerate(samples, 1):
print(f"\n样本 {i}:{sample}")
result = buggy_pipeline(sample)
print(f" → {result}")
输出示例:
样本 1:{'name': 'Alice', 'age': '30', 'score': 85, 'tags': ['vip']}
→ Alice(30 岁),归一化分数:0.85,标签:vip
样本 2:{'age': 'abc', 'score': '九十分'}
分数计算失败:unsupported operand type(s) for /: 'str' and 'int'
获取标签失败:'tags'
→ 匿名(0 岁),归一化分数:0.00,标签:None
样本 3:{}
年龄解析失败:invalid literal for int() with base 10: '未知'
分数计算失败:unsupported operand type(s) for /: 'NoneType' and 'int'
获取标签失败:'tags'
→ 匿名(0 岁),归一化分数:0.00,标签:None
样本 4:{'name': 'Bob', 'age': 25, 'score': 90, 'tags': []}
获取标签失败:list index out of range
→ Bob(25 岁),归一化分数:0.90,标签:None
上面的代码体现了防御性编程的思路:在每个可能出错的位置包一层 try,让程序即便遇到异常数据也能继续处理后续逻辑,而不是整体崩溃。下一节我们将系统学习 try-except 语句的所有用法。
🎯 动手练习
- 异常识别:编写代码分别触发
ValueError、TypeError、KeyError、IndexError,观察 Traceback 输出 - 继承链验证:使用
__mro__查看FileNotFoundError的完整继承链,验证它是OSError的子类 - 异常属性探索:捕获一个
OSError,打印errno、strerror、filename属性 - Traceback 阅读:编写三层函数调用,在最内层抛出异常,练习从底部往上读调用栈
📚 延伸阅读
- try-except 进阶:
else、finally、链式异常、ExceptionGroup - 自定义异常:继承
Exception创建业务异常类,携带额外上下文信息 - 日志记录:
logging.exception()自动记录异常堆栈 - 防御性编程:EAFP(请求原谅比许可容易)vs LBYL(三思而后行)
ValueError值类型正确但内容不合法int("abc")TypeError类型不匹配1 + "2"KeyError字典键不存在d["missing"]IndexError索引越界lst[999]FileNotFoundError文件不存在open("no.txt")PermissionError权限不足open("/root/x")ZeroDivisionError除零5 / 0AttributeError属性不存在"abc".uppercase()NameError变量名未定义print(x)ImportError模块导入失败import nonexistentStopIteration迭代器耗尽next(iter([]))RuntimeError一般运行时错误自定义运行时错误NotImplementedError功能未实现抽象方法占位KeyboardInterruptCtrl+C 中断用户主动中断SystemExit`sys.exit()` 触发程序主动退出查看继承链`Exception.__mro__`完整继承顺序异常属性`exc.args`异常参数元组OSError 属性`exc.errno`系统错误码附加说明`exc.add_note("msg")`3.11+ 特性✅ 本节总结
本节我们学习了异常的基础知识,核心要点包括:
- 异常是对象:描述程序运行中遇到的问题,可检查属性、附加 note、重新抛出
- 异常层次结构:
BaseException→Exception→ 具体异常,日常只捕获Exception及其子类 - 分组捕获:利用继承关系,捕获
LookupError能同时处理KeyError和IndexError - 常见内置异常:
ValueError(值不合法)、TypeError(类型不匹配)、KeyError(键不存在)等 - 阅读 Traceback:从最后一行看异常类型,从底部往上追调用栈,定位出错位置
- 主动抛异常:
raise务必附带清晰的信息,说明发生了什么以及期望是什么
下一节我们将学习 try-except 语句,掌握捕获、传递、链式抛出等进阶用法。