第八章|异常处理:当程序遇到“意外”时
本文介绍了Python异常处理的核心概念和方法。主要内容包括:异常的定义与常见内置异常类型;使用try-except捕获和处理异常,包括多异常捕获和获取异常实例;else和finally子句的使用场景;raise主动抛出异常的方法;如何创建自定义异常类;断言的正确使用方式及注意事项。文章还列举了异常处理的常见反模式,如空except块或捕获异常不处理等不良实践。通过类比生活中的意外情况,生动说明了
📌 引言:人生总有意外,程序也是
想象一下:你正在厨房精心准备晚餐,切菜时发现——刀不见了!你打开冰箱拿鸡蛋,结果盒子是空的!你打开燃气灶,发现煤气没开!
如果做饭没有预案,这些小意外就会让你手忙脚乱。同样,程序运行时也会遇到各种“意外”:
- 文件明明存在,打开时却被删了
- 用户输入了字符串,你却期待数字
- 网络请求突然超时
如果没有应对措施,程序就会直接崩溃,留下一堆看不懂的报错信息。异常处理就是给程序准备的“应急预案”,让它遇到意外时能优雅地处理,而不是直接躺平。
本章我们就来学习:
- 什么是异常
- 如何捕获和处理异常
- 如何主动抛出异常
- 如何定义自己的异常
- 断言这个小工具怎么用
一、异常是什么?—— 程序的“感冒发烧”
异常是程序运行过程中发生的错误或意外情况。当异常发生时,Python 会中断正常执行流程,并抛出异常对象。如果没有处理,程序就会终止并显示错误信息(traceback)。
# 一个经典的崩溃
print(10 / 0) # ZeroDivisionError: division by zero
常见的内置异常
| 异常 | 触发场景 |
|---|---|
ZeroDivisionError |
除以零 |
FileNotFoundError |
文件不存在 |
ValueError |
值类型正确但内容不合法(如 int(“abc”)) |
TypeError |
操作或函数应用于不适当类型 |
IndexError |
列表索引超出范围 |
KeyError |
字典键不存在 |
AttributeError |
访问不存在的属性 |
NameError |
使用未定义的变量 |
异常和错误的区别:错误通常指语法错误(程序根本跑不起来),异常是运行时发生的问题。但日常口语中常混用。
二、try-except:捕获异常,拯救程序
1️⃣ 基本语法
try:
# 可能出错的代码
result = 10 / 0
except ZeroDivisionError:
# 处理特定异常
print("除数不能为0!")
如果 try 块中发生了 ZeroDivisionError,程序会立即跳转到对应的 except 块执行,然后继续执行后面的代码(不会崩溃)。
2️⃣ 捕获多个异常
可以用元组同时捕获多种异常:
try:
num = int(input("请输入一个数字:"))
result = 100 / num
except (ValueError, ZeroDivisionError):
print("输入必须是非零数字!")
也可以用多个 except 块分别处理:
try:
num = int(input("请输入一个数字:"))
result = 100 / num
except ValueError:
print("这不是一个有效的数字!")
except ZeroDivisionError:
print("数字不能为零!")
3️⃣ 获取异常实例
如果想得到异常的具体信息,可以用 as e:
try:
with open('nofile.txt', 'r') as f:
content = f.read()
except FileNotFoundError as e:
print(f"文件未找到:{e}") # e 包含了错误详情
4️⃣ 捕获所有异常(谨慎!)
try:
# 可能出错的代码
except Exception as e:
print(f"出错了:{e}")
Exception 是所有常规异常的基类,这样能捕获大部分异常,但不推荐滥用,因为可能隐藏你没预料到的 bug,比如 KeyboardInterrupt(Ctrl+C)不会被捕获(它继承自 BaseException 而非 Exception)。更糟糕的是,except: 不带异常类型会捕获所有异常,包括 SystemExit 和 KeyboardInterrupt,让你连程序都关不掉。
最佳实践:只捕获你预料到并知道如何处理的异常。
三、else 和 finally:收尾工作
1️⃣ else:没有异常时执行
try:
num = int(input("请输入一个数字:"))
except ValueError:
print("输入不是数字")
else:
print(f"你输入了 {num},真棒!") # 只有没发生异常时执行
else 块在 try 没有抛出任何异常时执行,可以避免把“正常代码”也放进 try 块,减少误捕获的可能性。
2️⃣ finally:无论是否异常都执行
f = None
try:
f = open('test.txt', 'r')
content = f.read()
except FileNotFoundError:
print("文件不存在")
finally:
if f:
f.close() # 无论如何都要关闭文件
print("清理工作完成")
finally 块中的代码一定会执行,即使 try 或 except 中有 return、break、continue。常用于释放资源(关闭文件、数据库连接等)。
现代写法:资源管理大多用 with 语句,它自动处理了类似 finally 的清理工作,所以 finally 现在用得少了,但在某些场景(如手动加锁/解锁)仍然有用。
🧠 冷知识:try-except-else-finally 的执行顺序
- 如果
try正常 → 执行else→ 最后finally - 如果
try异常 → 执行匹配的except→ 最后finally - 如果
try或except中有return,finally依然会在返回前执行 - 如果
finally中有return,它会覆盖之前的return
def test():
try:
return "try"
finally:
return "finally" # 覆盖了 try 的 return
print(test()) # 输出 finally
别这么写,可读性极差。
四、raise:主动抛出异常
有时候程序逻辑自己发现了问题,你想主动制造一个异常,告诉调用者“出事了”。这时用 raise。
1️⃣ 抛出内置异常
def withdraw(balance, amount):
if amount > balance:
raise ValueError("余额不足")
return balance - amount
try:
new_balance = withdraw(100, 200)
except ValueError as e:
print(e) # 余额不足
2️⃣ 重新抛出异常
捕获异常后,可能部分处理,然后希望继续向上层抛出:
try:
process_data()
except ValueError:
log_error()
raise # 重新抛出当前异常,保留调用栈
单纯的 raise 不加参数会重新抛出当前异常,常用于在记录日志后继续传播。
3️⃣ 抛出自定义异常(见下一节)
五、自定义异常:打造你自己的“警报器”
当内置异常不足以描述你的业务错误时,可以创建自己的异常类。只需继承 Exception(或其子类)。
class BalanceError(Exception):
"""余额不足的自定义异常"""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"余额 {balance} 不足,需要 {amount}")
def withdraw(balance, amount):
if amount > balance:
raise BalanceError(balance, amount)
return balance - amount
try:
withdraw(100, 200)
except BalanceError as e:
print(e) # 余额 100 不足,需要 200
print(f"当前余额:{e.balance}") # 可以访问自定义属性
自定义异常让错误信息更丰富,也方便调用者针对性地捕获。
六、断言:快速调试的利器
断言用于检查“应该永远为真”的条件,如果条件为假,程序会抛出 AssertionError。
def divide(a, b):
assert b != 0, "除数不能为零"
return a / b
divide(10, 0) # AssertionError: 除数不能为零
使用场景
- 调试时检查参数合法性
- 验证程序内部状态
- 作为文档,表明某些假设
注意事项
- 断言可以用
-O命令行选项全局禁用(python -O script.py),所以不要用断言做数据验证或安全检查,验证用户输入应该用if并抛异常。 - 断言失败意味着程序有 bug,而不是运行时错误。
# ❌ 错误用法
def process(user_id):
assert user_id is not None, "user_id 不能为空"
# 如果禁用断言,user_id 为 None 时不会报错,导致后续崩溃
# ✅ 正确用法
def process(user_id):
if user_id is None:
raise ValueError("user_id 不能为空")
七、异常处理的“反模式”
❌ 空 except 或 except: 捕获所有异常
try:
# something
except:
pass # 什么都抓,连 Ctrl+C 都抓,程序关不掉!
❌ 捕获异常但不处理(pass)
try:
risky_operation()
except SomeError:
pass # 错误被吞了,调试时会很痛苦
至少应该记录日志。
❌ 用异常控制正常流程
# 糟糕的代码
try:
value = my_dict[key]
except KeyError:
value = default
应该用 if key in my_dict 或 get() 方法。异常用于意外情况,不是常规逻辑。
❌ 在 except 块里引发新异常时不保留原始信息
try:
convert()
except ValueError:
raise MyError("转换失败") # 丢失了原始异常信息
可以用 raise MyError("转换失败") from e 来保留原因。
八、异常处理的最佳实践
- 只捕获你打算处理的异常,不要过度捕获。
- 尽量具体:捕获
FileNotFoundError而不是Exception。 - 在合适的地方处理:底层函数让异常向上传播,顶层再捕获。
- 使用 finally 释放资源,但能用
with就用with。 - 自定义异常时继承 Exception,保持清晰。
- 用断言检查内部一致性,但不要依赖它做数据验证。
- 记录异常日志,便于排查问题。
✅ 本章总结
| 概念 | 说明 |
|---|---|
| 异常 | 运行时发生的错误 |
| try-except | 捕获并处理异常 |
| 多个 except | 分别处理不同类型的异常 |
| else | 无异常时执行 |
| finally | 无论是否异常都执行 |
| raise | 主动抛出异常 |
| 自定义异常 | 继承 Exception,增加业务语义 |
| assert | 调试用断言,生产可禁用 |
| 反模式 | 空 except、吞异常、滥用异常 |
记住:异常处理不是让程序不崩溃,而是让程序在崩溃时能优雅地告诉我们为什么,并有机会恢复或安全退出。
🚀 下集预告
掌握了异常处理,你的程序已经具备了基本的健壮性。下一章我们将进入 Python 最核心也最迷人的部分——面向对象编程。类、对象、继承、多态……这些概念将彻底改变你组织代码的方式。准备好拥抱 OOP 了吗?我们即将开启新世界的大门!
现在,去写点带异常处理的代码吧:试试写一个健壮的计算器,能处理各种非法输入;或者写个文件读取函数,遇到错误能给出友好提示。实践出真知!🛡️
🔗 上一篇:文件操作:与硬盘对话的艺术
🔗 下一篇:面向对象编程:把代码当成积木搭(待更新)
更多推荐


所有评论(0)