Skip to content

第 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.pytry-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)」。

三条关键性质:

  1. 异常是自动引发的——你写错代码,解释器替你把它「扔」出来;
  2. 引发异常时,当前代码块会立即中断,后面的语句不再执行;
  3. 异常是一种特殊对象,它有自己的类型、自己的信息。

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 / 0ZeroDivisionError
[1, 2, 3][5]IndexError
{"a": 1}["b"]KeyError
"abc" + 1TypeError
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 放行。

课后作业 ​

程序员小派在调试程序时,发现程序偶尔会抛出自定义异常,但是根据异常提示无法定位 异常产生的原因和产生异常的具体代码行数。作为小派的搭档,请你对该问题提出有效的代码优化方案。

参考答案(三个抓手):

  1. 携带足够信息:在自定义异常的 __init__ 里保存关键参数(如出错的值、行号),并 super().__init__(消息)——别只 pass 一个空异常。
  2. 保留栈回溯:捕获后重新引发用 raise(不要吞掉),必要时用 logging.exception() 或 traceback.format_exc() 把完整栈写进日志,里面有精确到行的调用链。
  3. 范围收窄 + 位置前置:把 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 要成对

三句话

  1. 异常不是灾难,是机制:它会在出错处自动引发、让代码块中断,并带上「类型 + 位置」的栈回溯。
  2. 捕获要位置准、范围窄、处理明:try 设在错误前,except 精确到类型,finally 把该关的关掉。
  3. 业务错误用自定义异常表达(继承 Exception),资源清理交给 with。

动手练习

  1. 跑通 code/ 下本章 6 个示例,对照真实输出读一遍报错。
  2. 把第 72 讲作业的 5 种异常,写成一张「写法 → 异常类型」对照表。
  3. 用自定义异常改写一个小场景(如登录密码过短、库存不足),让错误信息能直接告诉用户「哪里不对」。
  4. 把 code/75b-nested-syntax-error.py 的错误补好(给 try 配一个 except),体会语法错误与运行时错误的区别。

下一章预告 ​

第 10 章进入高级数据类型与算法:namedtuple、魔术方法、lru_cache、设计模式、 multiprocessing 等纯 Python 部分会真跑;Redis / MySQL 部分只讲原理与代码。


← 上一章:模块与标准库 | 返回总目录 | 下一章:高级数据类型与算法 →

内容基于开源课程《零基础学 Python》的 PPT 与配套代码(gitee.com/wilsonyin/zero-basics-python,LGPL-2.1)整理并重排为中文学习路径