第 10 章 异常处理与调试
学习目标
- 理解异常的概念,能读懂 traceback
- 认识常见异常类型
- 用
try/except/else/finally处理异常 - 掌握 print 调试与最小复现的排查方法
10.1 什么是异常
程序运行中出现的错误叫异常。第 1 章起我们见过的报错(NameError、TypeError 等),都是异常。异常发生时,解释器打印 traceback 并停止程序——除非我们用 try 提前拦截。
回顾 traceback 的读法(第 1 章「读报错三步」):
Traceback (most recent call last):
File "demo.py", line 2, in <module>
print(1 / 0)
ZeroDivisionError: division by zero最后一行是异常类型和异常信息,上面是出错的文件与行号。先看最后一行,再回代码定位,是排查的基本顺序。
10.2 常见异常类型
| 异常 | 含义 | 典型场景 |
|---|---|---|
NameError | 名字未定义 | 拼错变量名、变量未赋值 |
TypeError | 类型不匹配 | "a" + 1、参数个数不对 |
ValueError | 值不合法 | int("abc")、int("3.14") |
IndexError | 下标越界 | "hello"[10]、[1, 2][5] |
KeyError | 键不存在 | d["b"],字典里没有这个键 |
ZeroDivisionError | 除数为 0 | 10 / 0 |
FileNotFoundError | 文件不存在 | open("nofile.txt") |
能说出「哪个场景对应哪个异常」,是调试能力的基础。
10.3 try / except
try / except 让程序「试着执行一段代码,出错时按指定方式处理」:
try:
result = 10 / 0
except ZeroDivisionError:
print("除数不能为 0")输出:
除数不能为 0结构说明:
try块里的代码可能出错的部分。- 出错时,跳转到匹配的
except;except后面写要捕获的异常类型。 - 没出错时,
except不执行。
用 as e 拿到异常对象,输出异常信息:
try:
result = 10 / 0
except ZeroDivisionError as e:
print("捕获到异常:", e)输出:
捕获到异常: division by zero一个 try 可以接多个 except,分别处理不同类型的异常:
try:
a = float(input("请输入被除数: "))
b = float(input("请输入除数: "))
print(a / b)
except ZeroDivisionError:
print("除数不能为 0")
except ValueError:
print("请输入数字")输入 10 和 0 时输出 除数不能为 0;输入 abc 时输出 请输入数字。异常按从上到下的顺序匹配,匹配到第一个就停止。
10.4 else 与 finally
else:没有异常时执行(在 except 之后)。
finally:无论是否异常都执行,通常用来做清理工作(如关闭连接)。
try:
result = 10 / 2
except ZeroDivisionError as e:
print("捕获:", e)
else:
print("无异常,结果是", result)
finally:
print("finally 总是执行")输出:
无异常,结果是 5.0
finally 总是执行把上面的 10 / 2 改成 10 / 0 再看:输出会变成 捕获: division by zero 和 finally 总是执行,else 不执行。
10.5 调试技巧
异常拦截是「防守」,真正定位问题靠调试。三个常用方法:
1. print 调试。 在怀疑的位置打印变量的值,观察哪里与预期不符:
def average(scores):
total = 0
for s in scores:
total += s
print("累加中:", total) # 临时调试输出
return total / len(scores)2. 最小复现。 把出错的代码裁剪到最少、可以独立运行的程度,再运行它。这样排除了无关代码的干扰,也方便把「最小复现代码 + 报错」完整地描述给别人。
3. 二分定位。 在程序中间加一个 print("到这里了") 之类的标记:若输出了,说明错误在后面;否则在前面。反复对半缩小范围。
正式的断点调试(编辑器里点击行号暂停运行、逐步观察)是进阶技能,入门阶段 print 调试足够。
动手实践
任务一:带容错的温度换算。 用户输入非数字时重新询问,而不是崩溃:
while True:
try:
c = float(input("请输入摄氏温度: "))
break
except ValueError:
print("输入的不是数字,请重新输入")
f = c * 9 / 5 + 32
print(f"{c} 摄氏度等于 {f} 华氏度")while True + break 的搭配:只有输入合法时才 break 退出循环。
任务二:给第 3 章的计算器加上容错。 处理「除数为 0」和「输入非数字」两种情况:
try:
a = float(input("请输入第一个数: "))
b = float(input("请输入第二个数: "))
print(f"{a} / {b} = {a / b}")
except ZeroDivisionError:
print("除数不能为 0")
except ValueError:
print("请输入数字")常见错误
| 错误写法 | 说明 |
|---|---|
except:(不带类型)或 except Exception: 包住一切 | 会吞掉所有错误,连程序自身的 bug 也被隐藏,难以排查;尽量只捕获预期会发生的异常类型 |
把大段代码全塞进 try | 难以判断错误出在哪一行;让 try 只包住可能出错的少数几行 |
在 except 里只 pass | 异常被静默忽略,问题还会以更隐蔽的方式出现 |
捕获了异常却不看 e | as e 拿到信息有助于判断根因 |
章末练习
基础
- 写程序:读入两个数做除法,捕获
ZeroDivisionError并输出「除数不能为 0」。 - 写程序:尝试
int(input(...)),捕获ValueError并输出「请输入整数」。 - 指出下列代码会抛出什么异常:
print([1, 2][5])print(d["x"])(d 中没有键"x")open("no.txt")
提高
- 写函数
safe_divide(a, b),返回a / b的结果;除数为 0 时返回None。 - 写程序:循环读入成绩(输入
q结束),用try跳过非数字输入,最后输出所有合法成绩的平均分。
挑战
- 读入一个文件名,用
try尝试打开;文件不存在时提示「文件不存在」,并让用户重新输入,直到成功打开为止。 - 用 print 调试法,找出下面代码的错误并修复(不运行,先推理,再运行验证):python
def total_scores(text): scores = text.split(",") return sum(scores) print(total_scores("85,92,78"))
章末自测
每题选择一个最佳答案。本书不附答案:完成后交由老师或 AI 老师批改讲解。
- 除数为 0 会抛出的异常是?
- A.
ValueError - B.
ZeroDivisionError - C.
TypeError - D.
IndexError
- A.
- 下标越界会抛出的异常是?
- A.
KeyError - B.
NameError - C.
IndexError - D.
ValueError
- A.
- 字典键不存在会抛出的异常是?
- A.
KeyError - B.
IndexError - C.
TypeError - D.
FileNotFoundError
- A.
int("abc")会抛出?- A.
TypeError - B.
ValueError - C.
NameError - D.
KeyError
- A.
try块中代码没有出错时,else块?- A. 执行
- B. 不执行
- C. 报错
- D. 视情况
- 无论是否出错都执行的块是?
- A.
try - B.
except - C.
else - D.
finally
- A.
except ZeroDivisionError as e:中e是?- A. 异常类型
- B. 异常对象
- C. 数字
- D. 文件
- 一个
try可以有?- A. 只能一个
except - B. 多个
except - C. 不能有
except - D. 两个
finally
- A. 只能一个
- 捕获一切异常的写法是?
- A.
except: - B.
catch all: - C.
try except: - D.
exception:
- A.
- 关于「最小复现」,下列说法正确的是? A. 让代码尽可能短小并可独立运行 B. 让代码尽量长 C. 删除所有 print D. 只保留报错行
