Python 异常处理
真实程序一定会遇到意外:用户输入的不是数字、文件不存在、网络断了、API 超时。异常处理让程序"出了错也不崩溃",而是优雅地处理。这一章把 Python 异常机制讲透——从 try/except 到自定义异常、上下文管理器。
1. 为什么需要异常处理?
没有异常处理,程序一出错就直接崩溃退出,用户体验极差。异常处理让你捕获错误、记录日志、给用户友好提示、继续运行。它是健壮程序的基础。
2. try / except / else / finally
# 基础结构:try / except / else / finally
try:
n = int(input("请输入一个数字:"))
result = 10 / n
except ValueError:
# 捕获特定异常
print("那不是有效的数字!")
except ZeroDivisionError:
print("不能除以零!")
except Exception as e:
# 兜底:捕获所有其他异常
print(f"发生了未知错误:{e}")
else:
# 没异常时才执行(常用于"成功后的逻辑")
print(f"结果是 {result}")
finally:
# 无论是否有异常都会执行(常用于清理资源)
print("计算结束")
# 示例运行:
# 输入 5 -> 结果是 2.0 / 计算结束
# 输入 abc -> 那不是有效的数字! / 计算结束
# 输入 0 -> 不能除以零! / 计算结束四个块的作用:
- try:可能出错的代码。
- except:捕获异常并处理。可以有多个,从具体到一般排列。
- else:try 没异常时才执行。常用于"成功后的后续逻辑"。
- finally:无论是否有异常都会执行。常用于清理资源(关文件、释放锁)。
3. 常见内置异常
# Python 内置异常类(层次结构,部分)
# BaseException
# ├── SystemExit sys.exit() 触发
# ├── KeyboardInterrupt Ctrl+C 中断
# └── Exception 所有"普通"异常的基类
# ├── ValueError 值错误(如 int("abc"))
# ├── TypeError 类型错误(如 "a" + 1)
# ├── KeyError 字典 key 不存在
# ├── IndexError 索引越界
# ├── AttributeError 属性不存在
# ├── FileNotFoundError 文件不存在
# ├── ZeroDivisionError 除以零
# ├── NameError 变量未定义
# ├── StopIteration 迭代结束
# ├── OverflowError 数值溢出
# └── ... 还有很多
# 例子:各种常见异常
try:
x = [1, 2, 3]
print(x[10]) # IndexError
except IndexError:
print("索引越界")
try:
d = {"a": 1}
print(d["b"]) # KeyError
except KeyError as e:
print(f"key 不存在:{e}") # key 不存在:'b'
try:
f = open("不存在.txt") # FileNotFoundError
except FileNotFoundError:
print("文件不存在")记住这些常见异常能帮你快速读懂错误信息。所有异常都继承自 Exception(顶层是 BaseException,但日常只用 Exception 子类)。
4. raise:主动抛出异常
# raise:主动抛出异常
def divide(a, b):
if b == 0:
# 主动抛出,提前拦截错误
raise ValueError("除数不能为零")
return a / b
# 调用方需要处理这个异常
try:
result = divide(10, 0)
except ValueError as e:
print(f"出错:{e}") # 出错:除数不能为零
# raise 不带参数:重新抛出当前异常(在 except 块里)
try:
n = int("abc")
except ValueError:
print("记录日志...")
raise # 把异常继续往外抛,让上层处理主动抛异常用于"防御式编程":在函数入口检查参数,不合法就抛异常,避免错误传播到深处。
5. 自定义异常
真实项目里,内置异常不够用,需要自定义业务异常。规则:继承 Exception(不要继承 BaseException)。
# 自定义异常:继承 Exception 类
class WithdrawError(Exception):
"""取款异常的基类"""
pass
class InsufficientBalanceError(WithdrawError):
"""余额不足"""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(
f"余额不足:当前 {balance},要取 {amount}"
)
class NegativeAmountError(WithdrawError):
"""取款金额必须为正数"""
pass
class Account:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount <= 0:
raise NegativeAmountError("取款金额必须大于 0")
if amount > self.balance:
raise InsufficientBalanceError(self.balance, amount)
self.balance -= amount
return self.balance
# 使用
acc = Account(100)
try:
acc.withdraw(150)
except InsufficientBalanceError as e:
print(f"取款失败:{e}")
print(f"你的余额:{e.balance},尝试取 {e.amount}")
# 输出:
# 取款失败:余额不足:当前 100,要取 150
# 你的余额:100,尝试取 150自定义异常的好处:异常类型即业务语义——InsufficientBalanceError 比 ValueError 更能说明问题。可以携带额外信息(如余额、金额),供调用方处理。
6. 异常链:raise ... from
# 异常链:raise ... from 表示"由哪个异常引起"
def parse_and_double(s):
try:
n = int(s)
return n * 2
except ValueError as e:
# 把底层异常包装成更有意义的异常
raise ValueError(f"无法解析 '{s}' 为数字") from e
try:
parse_and_double("abc")
except ValueError as e:
print(f"主异常:{e}")
print(f"引起原因:{e.__cause__}")
# 输出:
# 主异常:无法解析 'abc' 为数字
# 引起原因:invalid literal for int() with base 10: 'abc'当你把底层异常"包装"成更高级的异常时,用 from 保留原因,方便调试。e.__cause__ 能拿到原始异常。
7. with 语句与上下文管理器
前面讲文件操作时,我们大量用了 with open(...)。它的本质是上下文管理器协议:任何实现 __enter__ 和 __exit__ 的类都能用 with。
# with 语句的本质:上下文管理器
# 任何实现了 __enter__ 和 __exit__ 的类都能用 with
class MyResource:
def __enter__(self):
print("获取资源")
return self # with ... as x 中的 x 就是返回值
def __exit__(self, exc_type, exc_val, exc_tb):
# exc_type/val/tb:如果有异常,这里是异常信息;没有就是 None
print("释放资源")
# 返回 True 表示"吞掉异常",返回 False/None 让异常继续传播
return False
def do_work(self):
print("使用资源")
# 用法
with MyResource() as r:
r.do_work()
# 输出:
# 获取资源
# 使用资源
# 释放资源
# open() 返回的文件对象就是一个上下文管理器
# 所以 with open(...) 能自动关闭文件自己写资源类(数据库连接、锁、网络会话)时,实现这两个方法就能用 with,自动管理资源生命周期。
8. 最佳实践(重要!)
# 异常处理的最佳实践
# 1. ⚠ 不要用裸 except(吞掉所有错误,隐藏 bug)
try:
do_something()
except: # ❌ 反例
pass # 错误被悄悄吞掉
# ✅ 应该捕获具体异常
try:
do_something()
except FileNotFoundError:
handle_not_found()
except ValueError as e:
handle_value_error(e)
# 2. 不要捕获后什么都不做(pass)
try:
risky()
except SomeError:
pass # ❌ 至少记日志或重新抛出
# 3. 异常信息要具体,不要丢失 traceback
import logging
try:
risky()
except SomeError as e:
logging.exception("操作失败") # 记录完整堆栈
raise # 重新抛出
# 4. 不要用异常处理做流程控制(性能差,代码乱)
# ❌ 反例:用 try/except 判断 key 是否存在
# try:
# value = d["key"]
# except KeyError:
# value = "default"
# ✅ 用 get
value = d.get("key", "default")核心原则:
- 捕获具体异常,不要裸
except:。 - 不要吞掉异常(
except: pass),至少记录日志。 - 不要用异常做流程控制(比如用 try/except 替代 if 判断)。
- 异常信息要具体,保留 traceback 用于调试。
- 该抛就抛:不知道怎么处理就别捕获,让上层处理。
小结
异常处理让程序健壮。重点掌握:try/except/else/finally、常见异常类型、raise、自定义异常(继承 Exception)、异常链 from、with 上下文管理器、最佳实践(捕获具体、不吞异常、不做流程控制)。下一篇看日期时间——Python 时间处理。
← 上一篇 Python 文件操作
下一篇 Python 日期时间 →