CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
异常处理
这一课你将能做到
用 try / except / else / finally 写出分层的异常处理,并精确捕获具体异常类型
用 raise 主动抛出异常、定义自己的异常类,让函数「出错时表达清楚」
面对报错 Traceback,能从下往上读出「异常类型、原因、出错行号」并定位问题
为什么要学
第 1 课你亲眼见过:Python 的错误要到运行那行才炸。程序只要和「外界」打交道——用户输入、文件、网络、数据库——就一定会遇到意外:文件不存在、网络超时、用户把年龄输成 "abc"。异常处理的本质是给意外预设好退路 :出错可以,但要知道为什么错、错了以后程序往哪走。裸奔的脚本遇到一次异常就整体崩溃,前面的处理全部白费。
人话:try 是「这步可能有风险」,except 是「真出事了我的预案」,else 是「没出事才走的路」,finally 是「无论如何都要做的收尾」,raise 是「我认为这不对,我要拉响警报」。
先建立图像
try: ← 派出侦察兵
危险操作()
except ValueError: ← 预案 A:针对「值不对」
处理 A()
except (KeyError, IndexError): ← 预案 B:可以一次接多种
处理 B()
else: ← 没触发任何预案时执行
正常收尾()
finally: ← 无论走哪条路都执行
必做的清理()
任何一层都没有预案 → 程序崩溃,打印 Traceback
Traceback 阅读法(从下往上读):
Traceback (most recent call last): ← 最上面是「调用路径」
File "app.py", line 5, in <module>
main() ← 谁调用了谁
File "app.py", line 2, in main
int("abc") ← 一路追到案发现场
ValueError: invalid literal for int() with base 10: 'abc'
↑ 最后一行 = 异常类型 + 信息(最重要)
跟着做
步骤 1:捕获第一个异常
新建 exception_demo.py:
raw = input("请输入一个整数:") # 故意输入 abc
try:
n = int(raw) # 这行可能抛 ValueError
print(f"你输入的是 {n}")
except ValueError: # 只接 ValueError
print("这不是合法整数,请重新运行")
print("程序没有崩溃,继续往下走")
运行并输入 abc,输出:
请输入一个整数:abc
这不是合法整数,请重新运行
程序没有崩溃,继续往下走
对比不包 try 时直接崩溃——except 把异常「接住」了,程序回到主干道。
步骤 2:精确捕获多种异常
config = {"host": "localhost"}
def read_config(key):
try:
value = config[key] # 可能 KeyError
return int(value) # 可能 ValueError
except KeyError:
print(f"配置项 {key} 不存在,使用默认值")
return None
except ValueError as e: # as e:把异常对象接住看详情
print(f"配置项 {key} 不是数字:{e}")
return None
print(read_config("host")) # localhost 不是数字 → None
print(read_config("port")) # 配置项不存在 → None
print(read_config("timeout")) # 演示前先补上它
配置项 host 不是数字:invalid literal for int() with base 10: 'localhost'
None
配置项 port 不存在,使用默认值
None
except 从上到下匹配,命中即走;把 int(value) 换成 float(value) 再试一次感受差异。一次接多种:except (KeyError, IndexError):。
步骤 3:else 与 finally 的执行时序 def safe_divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("除数不能为 0")
return None
else:
# 只有 try 完全成功才进来(放「成功路径」逻辑)
print(f"计算成功:{a} / {b} = {result}")
return result
finally:
# 无论如何都执行(放清理逻辑)
print("--- 本次计算结束 ---")
print(safe_divide(10, 2))
print(safe_divide(10, 0))
计算成功:10 / 2 = 5.0
--- 本次计算结束 ---
5.0
除数不能为 0
--- 本次计算结束 ---
None
执行顺序验证:失败路径也走了 finally,返回 None;成功路径 else → finally → return。
步骤 4:raise 主动抛出异常 def set_age(age):
if not isinstance(age, int):
raise TypeError(f"age 必须是 int,收到了 {type(age).__name__}")
if not 0 <= age <= 150:
raise ValueError(f"age 超出合理范围:{age}")
print(f"年龄已设置为 {age}")
set_age(18) # 正常
try:
set_age(-5) # 触发 ValueError
except ValueError as e:
print("捕获到:", e)
try:
set_age("18") # 触发 TypeError
except TypeError as e:
print("捕获到:", e)
年龄已设置为 18
捕获到: age 超出合理范围:-5
捕获到: age 必须是 int,收到了 str
raise 的信号意义:把「不合法」在错误现场立刻拦截 ,而不是返回 None 或 -1 让调用方猜。函数接口因此自文档化。
步骤 5:自定义异常类 class InsufficientBalanceError(Exception): # 继承 Exception 即为自定义异常
"""余额不足异常"""
def __init__(self, balance, amount):
super().__init__(f"余额 {balance},无法支取 {amount}")
self.balance = balance
self.amount = amount
def withdraw(balance, amount):
if amount > balance:
raise InsufficientBalanceError(balance, amount)
return balance - amount
try:
withdraw(100, 500)
except InsufficientBalanceError as e:
print(f"业务失败:{e}")
print(f"当前余额仍是 {e.balance}")
业务失败:余额 100,无法支取 500
当前余额仍是 100
自定义异常的价值:调用方可以「按业务语义」精确捕获,而不是和 ValueError 这类通用异常混在一起。命名规范:以 Error 结尾。
步骤 6:异常链与「不要吞异常」 import json
def load_config(path):
try:
with open(path, encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
raise RuntimeError(f"配置文件不存在:{path}") from None # 链式说明见下
# 错误示范:裸 except 吞掉一切
def bad_load(path):
try:
with open(path, encoding="utf-8") as f:
return json.load(f)
except Exception: # ⚠ 什么都接,连 KeyboardInterrupt 都想接
return {} # 出错了也一声不吭 —— 排障黑洞
print(load_config("不存在的.json")) # 会抛 RuntimeError,信息清晰
Traceback (most recent call last):
File "exception_demo.py", line XX, in <module>
print(load_config("不存在的.json"))
RuntimeError: 配置文件不存在:不存在的.json
三条铁律:①except 要写具体类型 ,不裸接 Exception;②接住后要么处理、要么记日志、要么重新 raise,别静默 return 默认值;③包装异常时 raise NewError(...) from e 保留原始现场(from None 表示刻意抹掉)。
讲清楚要点
异常是对象,层级是树 :所有异常继承 BaseException,业务异常都在 Exception 分支下(ValueError、KeyError、FileNotFoundError…)。except 按继承关系匹配——接 Exception 会接住几乎一切,所以只能放在兜底位。
try 块只包「可能出错的那几行」 :把无关代码塞进 try 会扩大误捕范围,也让 else 语义失效。粒度最小化是专业习惯。
except 顺序从具体到一般 :except FileNotFoundError 要写在 except OSError 前面(前者是后者的子类),否则子类永远轮不到。
else 放成功逻辑、finally 放清理 :finally 无视 return、break 甚至异常,一定执行——所以 with 语句(文件关闭)本质上就是 try/finally 的语法糖。
raise 三种姿势 :raise 异常对象(最常用)、raise(在 except 里原样重抛)、raise 新异常 from 旧异常(包装并保留现场)。
EAFP 风格 :Python 惯用「先做再说,错了再接」(Easier to Ask Forgiveness than Permission),对比先 if 判断再做的 LBYL。try: d[k] except KeyError 比 if k in d 在并发/重复判断场景更干净。
Traceback 是调试地图 :最后一行是异常类型和信息;往上每个 File ... line ... 是一帧调用栈。定位顺序:先读最后一行,再跳到对应 line 号看代码。
你可能会懵的地方
except Exception 和裸 except 有什么区别?
裸 except: 连 SystemExit、KeyboardInterrupt(Ctrl+C)都接,基本等于「禁用错误系统」;except Exception: 至少放行系统级信号。但两者都会吞掉具体原因,正确做法是列出预期异常类型,Exception 只允许出现在最顶层兜底 + 记日志的场景。
什么时候该捕获,什么时候让它崩?
能恢复的(用户输入错、可选文件缺失)→ 捕获处理;不能恢复的(配置全坏、代码 bug 如 AttributeError)→ 让它崩,崩溃信息本身就是最好的排障材料。「 catch 了但什么都不做」是最差的选择。
finally 里有 return 会怎样?
finally 的 return 会覆盖 try/except 里的返回值,还会吞掉正在传播的异常。绝对不要在 finally 里写 return——想清理就用 with 或只做释放动作。
raise 的时候传字符串行不行?
raise "出错了" 在 Python 3 直接 TypeError——raise 只接受异常类或异常实例。要么 raise ValueError("信息"),要么 raise ValueError(无信息版,会自动实例化)。
我捕获了异常还想保留日志继续跑,怎么写?
用标准库 logging:except ValueError as e: logging.exception("处理失败") —— logging.exception 自动附上完整 Traceback。print(e) 只有一行信息,丢掉了调用栈。
错法对照 错法 问题 正解 裸 except: 吞掉一切包括 Ctrl+C 捕获具体异常类型 except Exception: pass静默吞错,排障黑洞 记录日志或重新 raise try 包住整大段代码 误捕无关异常、定位困难 try 只包危险行 用返回值 -1/None 表示出错 调用方容易漏检查 出错直接 raise 异常对象 finally 里写 return 覆盖正常返回值、吞异常 finally 只做清理 raise "错误信息"TypeError:必须是异常类 raise ValueError("错误信息")except 写在父类后面且被父类遮蔽 子类分支永远走不到 从具体到一般排列 except
卡点急救
程序崩了满屏 Traceback :从最后一行读起——异常类型 + 信息;再往上找第一个「你自己文件」的 line 号(系统库帧可以跳过)。
异常接住了但变量报 UnboundLocalError :try 里赋值失败,变量没建成。把变量的默认值放在 try 之前:result = None 再进 try。
except 一直不命中 :类型对不上。用 Traceback 最后一行的类型名去精确匹配;不确定就临时 except Exception as e: print(type(e)) 看真实类型(调试完删掉)。
finally 意外执行了两次逻辑 :把业务 return 混进了 finally。finally 只留清理语句(关连接、删临时文件)。
想看完整调用栈但被 except 吞了 :在 except 里加 import traceback; traceback.print_exc(),或改用 logging.exception。
面试常问
try/except/else/finally 的完整执行顺序?
try 正常 → else → finally;try 抛异常且被 except 接住 → except → finally;未被接住 → finally 执行完后异常继续向上传播。finally 在任何分支都执行,包括 return 之前。
Python 的 EAFP 和 LBYL 是什么?
EAFP(先做,异常再处理):try: v = d[k] except KeyError;LBYL(先检查再做):if k in d: v = d[k]。Python 社区偏爱 EAFP,因为它少一次判断、且在竞态条件下更正确。这是面试考察「是否真写过 Python」的高频题。
raise 和 raise ... from ... 的区别?
raise 单独使用在 except 块内表示原样重抛当前异常;raise NewError() from old 显式包装并保留异常链(__cause__),Traceback 会显示 "The above exception was the direct cause...";from None 刻意切断链。链式异常用于「底层错误翻译成高层语义」。
自定义异常怎么写?为什么要自定义?
继承 Exception(或更贴切的内建异常)定义业务异常类,可携带额外字段(如余额、订单号)。价值:调用方可按业务语义精确 except;避免与通用异常混淆;日志与监控系统可按类型分类告警。
Java 受检异常 vs Python 异常模型的差异?
Java 区分受检/非受检异常,受检异常强制声明或捕获;Python 无受检概念,任何异常都可以不被声明,靠文档和测试保证。Python 的观点是「异常处理是运行时决策」,代价是漏处理只能运行时暴露——所以更要靠测试覆盖。
自测三问
try 里没有异常时,else 和 finally 的先后顺序?有异常且被接住时呢?
为什么 except FileNotFoundError 必须写在 except OSError 前面?
写出 InsufficientBalanceError 的定义(带 balance 字段)并在 withdraw 中 raise 它。
小结一句话 try/except 给意外留退路、raise 让错误在现场亮明身份、finally 保证收尾、Traceback 从下往上读——让程序「错得清楚、崩得体面」是脚本走向可靠的第一步。
延伸