第 10 章 异常处理
10.1 异常体系结构
Python 的异常是一个继承树,根节点是 BaseException:
BaseException
├── SystemExit # sys.exit() 触发
├── KeyboardInterrupt # Ctrl+C
├── GeneratorExit # 生成器关闭
└── Exception # 所有"正常"异常的基类
├── StopIteration
├── ArithmeticError
│ ├── ZeroDivisionError
│ ├── OverflowError
│ └── FloatingPointError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── OSError (IOError)
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── ConnectionError
├── ValueError
├── TypeError
├── AttributeError
├── NameError
├── ImportError
│ └── ModuleNotFoundError
├── RuntimeError
│ └── RecursionError
└── ...
重要原则:通常只捕获 Exception 的子类,不要捕获 BaseException(会吞掉 Ctrl+C)。
# 常见异常
int("abc") # ValueError
1 / 0 # ZeroDivisionError
[1, 2, 3][10] # IndexError
{"a": 1}["b"] # KeyError
open("不存在.txt") # FileNotFoundError
"hello" + 5 # TypeError
x = undefined_var # NameError
import nonexistent_module # ModuleNotFoundError
10.2 try / except / else / finally
try:
result = 10 / int(input("输入除数: "))
except ZeroDivisionError:
print("不能除以零")
except ValueError:
print("请输入有效数字")
except Exception as e:
print(f"未知错误: {e}")
else:
print(f"结果是: {result}") # 没有异常时执行
finally:
print("无论如何都会执行") # 总是执行(清理代码)
各子句的角色
try:
# 可能出错的代码
pass
except SomeError:
# 处理特定异常
pass
else:
# 没有异常时执行
# 把不需要异常保护的代码放这里,而不是 try 块中
pass
finally:
# 无论如何都执行 — 清理资源
# 即使有 return、break、continue 也会执行
pass
为什么用 else?
# 不好 — try 块太大,可能捕获意外异常
try:
data = json.loads(text)
process(data) # 如果这里也抛 ValueError,会被错误捕获
except ValueError:
print("JSON 解析失败")
# 好 — 精确捕获
try:
data = json.loads(text)
except ValueError:
print("JSON 解析失败")
else:
process(data) # 只有解析成功才执行
10.3 捕获多个异常与异常链
多个异常
# 分别处理
try:
value = my_dict[key]
except KeyError:
print("键不存在")
except TypeError:
print("键类型错误")
# 合并处理
try:
value = my_dict[key]
except (KeyError, TypeError) as e:
print(f"访问失败: {e}")
# 获取异常信息
try:
1 / 0
except ZeroDivisionError as e:
print(type(e)) # <class 'ZeroDivisionError'>
print(e) # division by zero
print(e.args) # ('division by zero',)
异常链(Exception Chaining)
# 隐式异常链 — 在处理异常时又发生新异常
try:
1 / 0
except ZeroDivisionError:
raise ValueError("计算失败")
# 输出会同时显示两个异常:
# ZeroDivisionError: division by zero
# During handling of the above exception, another exception occurred:
# ValueError: 计算失败
# 显式异常链 — raise ... from ...
try:
data = json.loads(bad_json)
except json.JSONDecodeError as e:
raise ValueError("配置文件格式错误") from e
# ValueError: 配置文件格式错误
# The above exception was the direct cause of the following exception:
# json.JSONDecodeError: ...
# 禁用异常链
try:
1 / 0
except ZeroDivisionError:
raise ValueError("计算失败") from None
# 只显示 ValueError,隐藏原始异常
10.4 自定义异常类
# 基本自定义异常
class AppError(Exception):
"""应用程序基础异常"""
pass
class ValidationError(AppError):
"""数据验证异常"""
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
class NotFoundError(AppError):
"""资源未找到异常"""
def __init__(self, resource, resource_id):
self.resource = resource
self.resource_id = resource_id
super().__init__(f"{resource}(id={resource_id}) 不存在")
# 使用
def create_user(name, age):
if not name:
raise ValidationError("name", "不能为空")
if age < 0 or age > 150:
raise ValidationError("age", f"无效的年龄: {age}")
return {"name": name, "age": age}
try:
user = create_user("", 25)
except ValidationError as e:
print(f"验证失败 - 字段: {e.field}, 消息: {e.message}")
异常层级设计
# 好的异常层级
class DatabaseError(Exception): pass
class ConnectionError(DatabaseError): pass
class QueryError(DatabaseError): pass
class IntegrityError(QueryError): pass
# 调用者可以按需捕获不同粒度
try:
db.execute(query)
except IntegrityError:
print("数据完整性错误") # 最具体
except QueryError:
print("查询错误") # 较通用
except DatabaseError:
print("数据库错误") # 最通用
10.5 异常组与 except*(Python 3.11+)
处理同时发生的多个异常,常见于并发场景:
# 创建异常组
eg = ExceptionGroup("多个错误", [
ValueError("值错误"),
TypeError("类型错误"),
KeyError("键错误"),
])
# except* 可以按类型匹配异常组中的异常
try:
raise eg
except* ValueError as e:
print(f"处理 ValueError: {e.exceptions}")
except* TypeError as e:
print(f"处理 TypeError: {e.exceptions}")
except* KeyError as e:
print(f"处理 KeyError: {e.exceptions}")
# 实际应用:并发任务中的异常处理
import asyncio
async def task(n):
if n == 2:
raise ValueError(f"任务 {n} 失败")
if n == 4:
raise TypeError(f"任务 {n} 类型错误")
return n * 10
async def main():
try:
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(task(i)) for i in range(5)]
except* ValueError as eg:
for e in eg.exceptions:
print(f"值错误: {e}")
except* TypeError as eg:
for e in eg.exceptions:
print(f"类型错误: {e}")
10.6 上下文管理器与异常抑制
from contextlib import suppress, contextmanager
# suppress — 安全忽略特定异常
with suppress(FileNotFoundError):
os.remove("可能不存在的文件.txt")
# 等价于
try:
os.remove("可能不存在的文件.txt")
except FileNotFoundError:
pass
# 自定义上下文管理器处理异常
@contextmanager
def handled_errors(*exceptions):
try:
yield
except exceptions as e:
print(f"已处理异常: {e}")
with handled_errors(ValueError, TypeError):
int("not a number")
# 输出: 已处理异常: invalid literal for int() with base 10: 'not a number'
10.7 LBYL vs EAFP 编程风格
两种处理错误的哲学:
LBYL — Look Before You Leap(三思而后行)
# 先检查再操作
if key in my_dict:
value = my_dict[key]
else:
value = default
if os.path.exists(filepath):
with open(filepath) as f:
data = f.read()
EAFP — Easier to Ask Forgiveness than Permission(先做再说)
# 先操作,出错再处理 — Python 推荐的风格
try:
value = my_dict[key]
except KeyError:
value = default
try:
with open(filepath) as f:
data = f.read()
except FileNotFoundError:
data = None
为什么 Python 偏好 EAFP?
- 更快:正常路径不需要额外检查(异常是特殊情况)
- 更安全:避免 TOCTOU(检查和使用之间状态变化)竞态条件
- 更简洁:减少条件分支嵌套
- 鸭子类型友好:不需要检查类型,直接用,不行再说
当然,也不是绝对的。简单的条件判断(如 if x is not None)用 LBYL 更直观。
10.8 warnings 模块
警告不是异常,不会中断程序执行:
import warnings
# 发出警告
warnings.warn("这个函数即将废弃", DeprecationWarning)
warnings.warn("性能可能较差", RuntimeWarning)
# 自定义警告
class SecurityWarning(UserWarning):
pass
warnings.warn("使用了弱密码", SecurityWarning)
# 控制警告行为
warnings.filterwarnings("ignore", category=DeprecationWarning) # 忽略
warnings.filterwarnings("error", category=RuntimeWarning) # 转为异常
# 临时控制
with warnings.catch_warnings():
warnings.simplefilter("ignore")
# 这里的警告会被忽略
warnings.warn("被忽略的警告")
在 deprecation 中的使用:
import warnings
from functools import wraps
def deprecated(message=""):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
warnings.warn(
f"{func.__name__} 已废弃。{message}",
DeprecationWarning,
stacklevel=2,
)
return func(*args, **kwargs)
return wrapper
return decorator
@deprecated("请使用 new_function() 代替")
def old_function():
return "old"
old_function()
# DeprecationWarning: old_function 已废弃。请使用 new_function() 代替
本章小结:异常处理是写出健壮代码的关键。遵循 EAFP 风格、精确捕获异常(不要裸
except)、善用else和finally、设计合理的异常层级是核心原则。Python 3.11 的异常组为并发场景下的错误处理提供了优雅的解决方案。