第 9 章 · 异常
程序出错不会「直接崩掉」——学会用
try接住错误、用raise表达业务规则,让程序优雅地应对意外。 本章 5 讲(072–076),从「认识异常」讲起,到「捕获」「自定义」「避坑」,最后做单元小结。
🎯 本章目标
- 理解异常是什么:错误自动引发、无法被解释器自动处理的特殊对象
- 看懂异常的层次结构,知道拿不准时用基类
Exception兜底 - 会用
try/except/else/finally捕获并处理异常 - 会写自定义业务异常(继承
Exception),用raise主动引发 - 会用
with语句(上下文管理器__enter__/__exit__)简化资源清理 - 记住编写异常处理程序时最容易踩的四类坑
本章代码
📌 代码约定:正文代码块都保持可直接运行;节选会明确标注「节选」并指向
code/完整文件。 所有输出都是真实运行结果(本机 Python 3.12.7)。 本章很多示例故意制造错误:能捕获展示的都用try/except包住(正常退出); 只有「语法错误」无法在运行时捕获,单独放在75b-nested-syntax-error.py里,退出码非 0 属正常。
| 文件 | 内容 |
|---|---|
code/72-exceptions.py | 异常的产生与中断、栈回溯、常见异常类型、层次结构(issubclass / MRO)、raise |
code/73-try-except.py | try-except / else / finally、多个 except、嵌套,以及「打开文件」作业 |
code/74-custom-exception.py | 自定义业务异常、raise、with 语句与上下文管理器,以及「文件过小」作业 |
code/75-pitfalls.py | 避坑指南:位置 / 范围 / 处理三类错误,附正确写法 |
code/75b-nested-syntax-error.py | 避坑 4:try 缺 except 的语法错误演示(故意写错,非零退出属正常) |
code/76-summary.py | 单元小结:把「认识 → 捕获 → 自定义 → with」串成一个小程序 |
第 72 讲 · 初识异常:异常的产生与分类
本节目标
搞清「异常到底是什么」,看懂报错信息里的异常类型和栈回溯,认识异常的层次结构。
知识要点
1. 什么是异常
异常(Exception):Python 解释器在执行程序的过程中,出现错误时的一种管理机制。它会在错误被监测到的位置被「引发(raise)」。
三条关键性质:
- 异常是自动引发的——你写错代码,解释器替你把它「扔」出来;
- 引发异常时,当前代码块会立即中断,后面的语句不再执行;
- 异常是一种特殊对象,它有自己的类型、自己的信息。
2. 栈回溯(Traceback)
除
SystemExit外,当异常没有被完全处理时,Python 都会打印栈回溯信息:告诉你「哪一行、哪一种错误」。
读报错要从下往上读:最后一行是错误类型 + 原因(如 ZeroDivisionError: division by zero),往上是出错位置和调用链。
3. 异常的分类(层次结构)
所有异常的根是 BaseException。它的下面分为两大类:
| 基类 | 含义 |
|---|---|
BaseException | 所有内置异常的基类 |
Exception | 所有内置的非系统退出类异常都派生自它,所有用户自定义异常也应当派生自它 |
再往下还有几个常用的中间基类:
ArithmeticError—— 各种算术类错误的父类(如ZeroDivisionError)LookupError—— 映射或序列所用「键/索引无效」时引发的父类(如IndexError、KeyError)OSError—— 操作系统相关错误的父类(如FileNotFoundError)
简化的继承树:
BaseException
├── SystemExit (系统退出,不属于 Exception)
├── KeyboardInterrupt (Ctrl+C 中断,不属于 Exception)
└── Exception
├── ArithmeticError ── ZeroDivisionError
├── LookupError ─┬──── IndexError
│ └──── KeyError
├── ValueError
├── TypeError
└── OSError ────────── FileNotFoundError📌 小结:异常是错误自动引发、无法被解释器自动处理的特殊对象;它有层次结构, 拿不准要捕获什么时,就用基类
Exception兜底。
代码实操
完整代码见 code/72-exceptions.py(含全部 import),运行:
bash
$ python3 72-exceptions.py真实输出(路径已简写为 /path/to/...):
text
========================================================
示例 1:异常会让代码「中断」
========================================================
第一行:程序开始
捕获到异常:division by zero (类型:ZeroDivisionError)
最后一行:异常被处理后,程序还能继续往下走
========================================================
示例 2:未被处理的异常会打印「栈回溯(Traceback)」
========================================================
上一段程序有异常
division by zero
---- 完整栈回溯(真实报错长这样)----
Traceback (most recent call last):
File "/path/to/09-exceptions/code/72-exceptions.py", line 36, in <module>
num / num2
~~~~^~~~~~
ZeroDivisionError: division by zero
========================================================
示例 3:异常是「对象」
========================================================
异常对象本身: ZeroDivisionError('division by zero')
它的类型 : <class 'ZeroDivisionError'>
转成字符串 : division by zero
是 BaseException 的实例? True
是 Exception 的实例? True
========================================================
示例 4:常见异常类型一览
========================================================
除零 → ZeroDivisionError: division by zero
下标越界 → IndexError: list index out of range
键不存在 → KeyError: 'b'
类型错误 → TypeError: can only concatenate str (not "int") to str
值错误 → ValueError: invalid literal for int() with base 10: 'abc'
========================================================
示例 5:异常的层次结构(用 issubclass 验证)
========================================================
ZeroDivisionError 是 ArithmeticError 的子类? True
IndexError 是 LookupError 的子类? True
KeyError 是 LookupError 的子类? True
LookupError 是 Exception 的子类? True
ValueError 是 Exception 的子类? True
Exception 是 BaseException 的子类? True
SystemExit 是 Exception 的子类? False
SystemExit 是 BaseException 的子类? True
========================================================
示例 6:FileNotFoundError 的继承链(MRO)
========================================================
FileNotFoundError -> OSError -> Exception -> BaseException -> object
========================================================
示例 7:用 raise 主动引发一个异常
========================================================
主动 raise 的异常被捕获: value error !!!!💡 注意示例 5 里
SystemExit不是Exception的子类(它是BaseException的直接子类)—— 这正是后面「避坑」里要讲的点:except Exception故意不接管系统退出和 Ctrl+C。
课后作业
请通过学习过的列表、字典、数字、字符串等数据类型,以及
if、for、while等条件控制语句, 自行编写程序,引发至少 5 个异常并查看异常类型。
参考答案(code/72-exceptions.py 的示例 4 就是这份作业):用一个 (说明, lambda: 出错代码) 列表,逐个放进 try 里跑,读出 type(exc).__name__。可引发:
| 写法 | 异常类型 |
|---|---|
1 / 0 | ZeroDivisionError |
[1, 2, 3][5] | IndexError |
{"a": 1}["b"] | KeyError |
"abc" + 1 | TypeError |
int("abc") | ValueError |
第 73 讲 · 异常捕获:出现异常时,如何利用程序进行处理?
本节目标
学会用 try-except 把「会出错的代码」包起来,并用 else / finally / 多个 except 控制处理流程。
知识要点
1. try-except 代码块
python
try:
可能产生异常的代码
except 异常:
捕获指定异常后运行的代码含义:试探着执行 try 里的代码;一旦引发 except 指定的那种异常,就跳到 except 里处理,程序不会崩。
2. else 代码块
python
try:
可能产生异常的代码
except 异常:
捕获后运行的代码
else:
try 部分没抛出异常,才执行这里的代码else 是「一切顺利」时才走的分支,常用来放「只有成功才该做的事」。
3. finally 代码块
python
try:
可能产生异常的代码
except 异常:
捕获后运行的代码
finally:
无论是否抛出异常,该部分代码均会执行finally 常用来收尾:关闭文件、释放连接。即使 try 里出了异常、或者被 return 提前结束,它依然会执行。
4. 捕获多个异常
一个 try 可以配多个 except,从上到下依次匹配:
python
try:
可能产生异常的语句
except 异常1:
处理方式一
except 异常2:
处理方式二也可以嵌套使用:
python
try:
try:
语句
except:
pass
except:
pass⚠️ 匹配是自上而下、先到先得。如果把父类(如
Exception)写在前面,后面的子类except永远轮不到。 所以经验法则:具体的异常写前面,宽泛的基类写最后。
代码实操
完整代码见 code/73-try-except.py,运行:
bash
$ python3 73-try-except.py真实输出(示例 5 的临时文件路径已简写为 .../):
text
========================================================
示例 1:最基本的 try-except
========================================================
尝试计算 1 / 0 ...
上一段程序有异常: division by zero
========================================================
示例 2:try-except-else-finally 完整流程
========================================================
divide(10, 2) → else :结果 = 5.0
divide(10, 2) → finally:这一段一定执行
divide(10, 0) → except:division by zero
divide(10, 0) → finally:这一段一定执行
========================================================
示例 3:捕获多个异常(多个 except,按顺序匹配)
========================================================
'123' 转换成功,结果是 123
'abc' 不是合法整数(ValueError)
None 类型不对,无法转换(TypeError)
========================================================
示例 4:嵌套 try-except
========================================================
外层 try 开始
内层 try 开始
内层捕获: division by zero
内层处理完了,外层继续往下走
========================================================
示例 5:课后作业 —— 打开文件时的异常捕获
========================================================
① 打开一个「不存在」的文件:
文件不存在,无法打开:.../不存在的文件.txt
② 打开一个「存在」的文件(正常读取):
['你好,异常处理!\n', '第二行。\n']课后作业
在文件操作过程中,使用
open()函数打开文件时,有可能会出现该文件在磁盘中不存在的异常, 请编写try-except语句,对文件打开的异常进行异常捕获。 * 当出现异常时,需关闭文件;无异常时正常读取文件。
参考答案(即代码实操的示例 5):用 FileNotFoundError 精确捕获「文件不存在」;把 f.close() 放进 finally,保证无论成功还是失败、只要文件被打开过就一定关掉。
python
def read_file(path):
f = None
try:
f = open(path, mode="r", encoding="utf-8")
return f.readlines()
except FileNotFoundError as e:
return f"文件不存在,无法打开:{e.filename}"
finally:
if f is not None:
f.close()第 74 讲 · 自定义异常捕获:如何定义业务异常?
本节目标
学会定义自己的异常类来表达业务规则,并用 with 语句简化资源清理。
知识要点
1. 自定义异常的用途
借用 Python 强大的异常处理功能,编写程序中断逻辑——继承现有的异常,并为它增加额外功能。
一句话:内置异常描述的是「程序层面的错误」(除零、越界);业务异常描述的是「规则层面的错误」 (余额不足、用户名违规、文件太小)。后者能让报错更贴近你的业务。
2. 自定义异常的方法
自定义异常需要继承异常的基类,一般通过继承 Exception 实现:
python
class MyException(Exception):
...
# 主动引发
try:
raise MyException
except MyException as e:
print(e)实战里通常会在 __init__ 里保存业务信息,并用 super().__init__(...) 交给父类,这样 str(e) 就能打印出原因。
3. with 语句
为了简化异常处理逻辑,Python 引入了
with语句。以打开文件为例:
python
with open("/path/to/file.txt") as f:
txt = f.readlines()离开 with 代码块时,文件自动关闭——不用手写 close(),也不用担心忘记关。
with 背后靠两个魔术方法实现:
| 方法 | 何时调用 | 作用 |
|---|---|---|
__enter__() | 进入 with 块时 | 返回值交给 as 后面的变量 |
__exit__() | 离开 with 块时 | 收尾清理;能拿到块里发生的异常信息 |
自定义 with 语句的写法:
python
class MyClass:
def __enter__(self):
...
def __exit__(self, exc_type, exc_value, tb):
...
with MyClass() as mc:
mc.xxx()📌 小结:自定义异常必须继承
Exception;它可以用raise让程序中断;with语句能简化异常处理与资源清理。
代码实操
完整代码见 code/74-custom-exception.py,运行:
bash
$ python3 74-custom-exception.py真实输出:
text
========================================================
示例 1:定义一个最简单的业务异常
========================================================
捕获到自定义异常: 名字不允许使用「jerry」
str(ne) 得到的原始信息: jerry
========================================================
示例 2:用自定义异常表达业务规则(银行取款)
========================================================
取款成功:余额 100 → 70
取款失败:余额 100 元,不足以取出 250 元(还差 150 元)
========================================================
示例 3:with 语句与上下文管理器(__enter__ / __exit__)
========================================================
__enter__ 被调用:进入 with 代码块,返回值交给 as 后面的变量
hello() 被调用:我正在 with 代码块里工作
__exit__ 被调用:with 代码块结束(正常或异常都会走这里)
========================================================
示例 4:__exit__ 如何收到 with 里的异常
========================================================
进入 with
准备 raise ...
退出时发现异常:ValueError: 我在 with 里出错啦
程序没有崩溃,继续执行
========================================================
示例 5:with open(...) 自动关闭文件
========================================================
读到的内容: with 会在代码块结束后自动关闭文件,省去了手写 close()。
离开 with 后文件是否已关闭: True
========================================================
示例 6:课后作业 —— 文件过小则抛自定义异常
========================================================
抛出异常:文件只有 2 个字符,少于要求的 10 个
正常输出: 这是一个够长的文件内容,已经超过十个字符了。💡 示例 4 里
__exit__返回了True,表示「异常已被我处理」,于是异常不再往上抛、程序继续往下跑。 返回False(或什么都不写)则照常把异常抛出去。
课后作业
请编写自定义异常类:如果读取的文件小于 10 个字符,抛出「文件过小」的自定义异常; 如果读取的文件大于等于 10 个字符,则正常输出该文件内容。
参考答案(即代码实操的示例 6):
python
class FileTooSmallError(Exception):
"""自定义异常:文件内容太少。"""
def read_file(path, min_chars=10):
with open(path, encoding="utf-8") as f:
content = f.read()
if len(content) < min_chars:
raise FileTooSmallError(f"文件只有 {len(content)} 个字符,少于要求的 {min_chars} 个")
return content第 75 讲 · 避坑指南:编写捕获异常程序时经常出现的问题
本节目标
识别并避开编写异常处理代码时的四类常见错误:位置、范围、处理方式、嵌套语法。
知识要点
坑 1:捕获位置设置不当
只要在猎物的必经之路上设陷阱,一定可以捕到猎物。
try-except设得太晚,程序已经在前面报错退出了,陷阱白设;- 受
if、while等分支影响,某条路径根本没执行到try-except。
原则:陷阱要设在错误发生之前。
坑 2:捕获范围设置不当
- 捕获范围过大:一上来就
except Exception,抛出异常时无法定位原因; - 用
Exception基类捕获,原因不明确。
原则:能精确到具体异常类型就别用基类;基类只放在最后兜底。
⚠️ 补充:
except Exception(甚至裸except:)会吞掉很多你想知道的信息。 好在KeyboardInterrupt(Ctrl+C)和SystemExit不是Exception的子类, 它们会「穿透」普通的except Exception——所以别写裸except:,否则连 Ctrl+C 都按不动。
坑 3:捕获处理设置不当
- 捕获后应当明确告知用户异常原因,而不是只丢一句「出错了」;
- 能由程序自动处理的要处理干净,比如关闭文件,防止数据丢失。
坑 4:嵌套 try-except 语法错误
try和except(或finally)必须成对出现;- 嵌套容易漏写,建议用 IDE 的自动缩进与语法检查来避免。
📌 小结:位置、范围、处理方式用错,是异常处理里最常见的三类错误; 处理语句一长就容易出语法错误,让 IDE 帮你检查。
代码实操
① 三类「逻辑坑」——完整代码见 code/75-pitfalls.py,运行:
bash
$ python3 75-pitfalls.py真实输出:
text
========================================================
坑 1:捕获位置设置不当 —— try 设得太晚
========================================================
准备访问 data[5](这里会出错)...
正确做法:把 try 设在错误发生之前,才能在错误处接住它 → list index out of range
========================================================
坑 2:捕获范围设置不当 —— 用 Exception 一把抓
========================================================
宽泛捕获结果: 出错了:IndexError(太宽泛,看不出原因)
精确捕获结果: 精确捕获:IndexError: list index out of range
========================================================
坑 2b:except Exception 不会捕获 KeyboardInterrupt
========================================================
KeyboardInterrupt 穿透了 except Exception,被外层接住 ——
正因如此,不要写「裸」的 except:,否则连 Ctrl+C 都按不动
========================================================
坑 3:捕获处理设置不当 —— 只说「出错了」,不告知原因/不关文件
========================================================
友好提示: 找不到报表文件「根本不存在的报表.txt」,请先上传后再试
========================================================
坑 4:嵌套 try-except —— 正确的写法
========================================================
外层 try
内层已处理: invalid literal for int() with base 10: 'abc'
内层处理完,外层若无异常继续
小结:位置、范围、处理方式三处用错,是异常处理里最常见的错误;
嵌套时务必保证 try 与 except 成对出现,靠 IDE 的自动缩进/检查来避免语法错误。② 第四类「语法错误」——语法错误在加载阶段就被解释器拦下,无法用 try 在运行时捕获,所以单独放一个文件。完整代码见 code/75b-nested-syntax-error.py(故意写错),运行:
bash
$ python3 75b-nested-syntax-error.py真实输出(路径已简写):
text
File "/path/to/09-exceptions/code/75b-nested-syntax-error.py", line 12
# ← 这里缺了一个 except(或 finally),Python 会在解析时报语法错误
SyntaxError: expected 'except' or 'finally' block⚠️ 这个文件故意缺少
except,退出码非 0 是正常的(它演示的就是「语法错误抓不住」)。 校验脚本scripts/verify-examples.py已把它登记进ALLOW_FAIL放行。
课后作业
程序员小派在调试程序时,发现程序偶尔会抛出自定义异常,但是根据异常提示无法定位 异常产生的原因和产生异常的具体代码行数。作为小派的搭档,请你对该问题提出有效的代码优化方案。
参考答案(三个抓手):
- 携带足够信息:在自定义异常的
__init__里保存关键参数(如出错的值、行号),并super().__init__(消息)——别只pass一个空异常。 - 保留栈回溯:捕获后重新引发用
raise(不要吞掉),必要时用logging.exception()或traceback.format_exc()把完整栈写进日志,里面有精确到行的调用链。 - 范围收窄 + 位置前置:把
except Exception换成具体的自定义异常类型,并把try挪到错误真正发生的位置,避免「宽泛捕获 + 位置过晚」双双掩盖问题。
第 76 讲 · 再回首:“异常处理”单元小结
本节目标
把「认识异常 → 捕获 → 自定义 → with」串起来,回顾本章知识地图。
知识要点
本章的知识地图,可以浓缩成一条主线:
① 认识异常 异常是错误自动引发、无法被解释器自动处理的特殊对象;有层次结构
↓
② 捕获异常 try-except 接住错误;else 走「顺利」分支;finally 一定收尾
↓
③ 自定义异常 继承 Exception → raise 引发 → except 捕获;with 简化资源清理
↓
④ 避坑 位置要早、范围要窄、处理要明、try/except 要成对速记口诀:「位置准、范围窄、处理明,try/except 成对出」。
代码实操
完整代码见 code/76-summary.py,它把四步串成一个小程序,运行:
bash
$ python3 76-summary.py真实输出:
text
========================================================
单元小结:异常处理四步走
========================================================
[1] 捕获自定义异常 + finally 清理:
年龄 18 合法
finally:不管成功失败,这一步都执行
捕获异常: 年龄不能为负数,收到的是 -3
finally:不管成功失败,这一步都执行
[2] with 语句自动管理资源:
读取: 用 with 打开文件,离开代码块就自动关闭。
文件已关闭? True
[3] 关键结论:
· 异常是错误自动引发、无法被解释器自动处理的特殊对象
· 拿不准要捕获什么,就用基类 Exception 兜底
· 自定义异常必须继承 Exception,用 raise 引发、except 捕获
· with 语句靠 __enter__ / __exit__ 实现,简化资源清理
· 捕获要「位置准、范围窄、处理明」,避免吞掉异常课后作业
回到本章开始的那句话:能捕获的用 try/except 接住,捕获不了的要如实暴露。 请你自己动手:把这四讲里「故意报错」的示例各挑一个,改成正确捕获后再运行一遍,体会「程序不崩,但错误被看清」的效果。
本章小结
速查表
| 主题 | 关键点 |
|---|---|
| 异常是什么 | 错误自动引发、无法被解释器自动处理的特殊对象;引发时当前代码块中断 |
| 栈回溯 | 除 SystemExit 外,未处理的异常都会打印 Traceback;从下往上读(类型 + 位置) |
| 异常层次 | 根是 BaseException;自定义异常与绝大多数内置异常都继承 Exception |
| 拿不准捕获什么 | 用基类 Exception 兜底(但别写裸 except:) |
try-except | 接住指定异常,程序不崩 |
else | 只有 try 没出错才执行 |
finally | 无论是否出错都执行,用于收尾(关文件) |
多个 except | 自上而下匹配,具体异常写前、基类写后 |
| 自定义异常 | class MyException(Exception),用 raise 引发、except 捕获 |
with 语句 | 靠 __enter__ / __exit__ 实现,自动清理资源(如自动关文件) |
| 避坑四则 | 位置要早、范围要窄、处理要明、try/except 要成对 |
三句话
- 异常不是灾难,是机制:它会在出错处自动引发、让代码块中断,并带上「类型 + 位置」的栈回溯。
- 捕获要位置准、范围窄、处理明:
try设在错误前,except精确到类型,finally把该关的关掉。 - 业务错误用自定义异常表达(继承
Exception),资源清理交给with。
动手练习
- 跑通
code/下本章 6 个示例,对照真实输出读一遍报错。 - 把第 72 讲作业的 5 种异常,写成一张「写法 → 异常类型」对照表。
- 用自定义异常改写一个小场景(如登录密码过短、库存不足),让错误信息能直接告诉用户「哪里不对」。
- 把
code/75b-nested-syntax-error.py的错误补好(给try配一个except),体会语法错误与运行时错误的区别。
下一章预告
第 10 章进入高级数据类型与算法:namedtuple、魔术方法、lru_cache、设计模式、 multiprocessing 等纯 Python 部分会真跑;Redis / MySQL 部分只讲原理与代码。