Python 异常处理
异常是程序运行时发生的错误。本章将介绍如何处理异常。
什么是异常?
当程序出现错误时,Python 会抛出异常。如果不处理,程序会终止。
print(1 / 0) # ZeroDivisionError: division by zero
# 列表越界
my_list = [1, 2, 3]
print(my_list[10]) # IndexError: list index out of range
# 类型错误
print("hello" + 123) # TypeError: can only concatenate str (not "int") to str
常见异常类型
| 异常类型 | 描述 |
|---|---|
| SyntaxError | 语法错误 |
| NameError | 名称错误(变量未定义) |
| TypeError | 类型错误 |
| ValueError | 值错误 |
| IndexError | 索引错误 |
| KeyError | 键错误 |
| ZeroDivisionError | 除零错误 |
| FileNotFoundError | 文件不存在 |
| ImportError | 导入错误 |
| AttributeError | 属性错误 |
try-except 结构
基本语法
try:
# 可能发生异常的代码
result = 10 / 0
except ZeroDivisionError:
# 处理特定异常
print("除数不能为零")
多个 except
try:
# 可能发生多种异常的代码
num = int(input("请输入一个数字:"))
result = 10 / num
except ValueError:
print("请输入有效的数字")
except ZeroDivisionError:
print("除数不能为零")
捕获所有异常
try:
# 可能发生异常的代码
result = 10 / 0
except Exception as e:
print(f"发生错误:{e}")
访问异常信息
try:
result = 10 / 0
except Exception as e:
print(f"异常类型:{type(e).__name__}")
print(f"异常信息:{e}")
print(f"异常描述:{e.args}")
else 子句
如果 try 块没有发生异常,执行 else 块:
try:
num = int(input("请输入一个数字:"))
result = 10 / num
except ValueError:
print("请输入有效的数字")
except ZeroDivisionError:
print("除数不能为零")
else:
print(f"结果是:{result}")
finally 子句
无论是否发生异常,都会执行 finally 块:
try:
file = open("test.txt", "r")
content = file.read()
except FileNotFoundError:
print("文件不存在")
finally:
# 清理工作
if 'file' in locals():
file.close()
print("文件已关闭")
主动抛出异常
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}")
自定义异常
class MyError(Exception):
"""自定义异常类"""
def __init__(self, message):
self.message = message
super().__init__(self.message)
# 使用自定义异常
def validate_age(age):
if age < 0:
raise MyError("年龄不能为负数")
if age > 150:
raise MyError("年龄超出合理范围")
return True
try:
validate_age(-5)
except MyError as e:
print(f"自定义异常:{e.message}")
异常链
异常传播
异常可以向上传播:
def level3():
return 1 / 0
def level2():
return level3()
def level1():
try:
result = level2()
except Exception as e:
print(f"在 level1 捕获:{e}")
raise # 重新抛出异常
try:
level1()
except Exception as e:
print(f"最终捕获:{e}")
异常链(Python 3)
使用 raise ... from 创建异常链,保留原始异常的上下文信息:
def parse_config(file_path):
try:
with open(file_path, 'r') as f:
content = f.read()
return json.loads(content) # 假设这里会失败
except FileNotFoundError as e:
# 将底层异常包装为更具体的应用层异常
raise ConfigError(f"配置文件不存在: {file_path}") from e
except json.JSONDecodeError as e:
raise ConfigError(f"配置文件格式错误: {file_path}") from e
class ConfigError(Exception):
"""配置错误"""
pass
# 异常链保留了完整的错误信息
try:
config = parse_config("app.json")
except ConfigError as e:
print(f"配置错误: {e}")
print(f"原始异常: {e.__cause__}") # 访问原始异常
异常链的工作原理
当使用 raise ... from 时:
- 新异常会被抛出
- 原始异常存储在新异常的
__cause__属性中 - 打印异常时会显示完整的异常链
try:
try:
int("not a number")
except ValueError as e:
raise RuntimeError("转换失败") from e
except RuntimeError as e:
print(f"当前异常: {e}")
print(f"原因: {e.__cause__}")
print(f"原因类型: {type(e.__cause__)}")
隐式异常链
如果在 except 块中处理异常时发生了新的异常,Python 会自动创建异常链:
def process_data(data):
try:
result = 1 / data['value']
except ZeroDivisionError:
# 这里如果 logging 出错,会形成隐式异常链
logging.error("除零错误")
raise # 重新抛出
# 隐式异常链存储在 __context__ 属性中
禁止异常链
使用 raise ... from None 可以禁止异常链:
try:
risky_operation()
except LowLevelError:
# 不想让调用者看到底层错误细节
raise UserFriendlyError("操作失败,请稍后重试") from None
异常组(Python 3.11+)
Python 3.11 引入了异常组(Exception Groups),允许同时处理多个不相关的异常。
为什么需要异常组
传统的异常处理一次只能处理一个异常,但在并发编程或批量操作中,可能同时发生多个不相关的异常:
# 传统方式:只能看到第一个异常
for task in tasks:
try:
task.run()
except Exception as e:
# 如果多个任务失败,只能捕获第一个
handle_error(e)
ExceptionGroup 基础
ExceptionGroup 可以包含多个异常:
# 创建异常组
eg = ExceptionGroup("多个错误发生", [
ValueError("无效的值"),
TypeError("类型错误"),
KeyError("missing_key")
])
# 抛出异常组
raise eg
使用 except* 匹配异常组
except* 用于匹配异常组中的特定类型异常:
try:
raise ExceptionGroup("任务执行失败", [
ValueError("任务1失败"),
TypeError("任务2失败"),
ValueError("任务3失败"),
RuntimeError("任务4失败")
])
except* ValueError as eg:
# 捕获所有 ValueError
print(f"值错误: {eg.exceptions}") # 包含两个 ValueError
except* TypeError as eg:
# 捕获所有 TypeError
print(f"类型错误: {eg.exceptions}")
except* RuntimeError as eg:
# 捕获所有 RuntimeError
print(f"运行时错误: {eg.exceptions}")
实际应用场景
并发任务错误处理
import asyncio
async def run_tasks():
tasks = [
fetch_data("url1"),
fetch_data("url2"),
fetch_data("url3")
]
results = await asyncio.gather(*tasks, return_exceptions=True)
# 收集所有异常
exceptions = [r for r in results if isinstance(r, Exception)]
if exceptions:
raise ExceptionGroup("多个请求失败", exceptions)
try:
asyncio.run(run_tasks())
except* ConnectionError as eg:
print(f"连接错误: {len(eg.exceptions)} 个")
except* TimeoutError as eg:
print(f"超时错误: {len(eg.exceptions)} 个")
数据验证
def validate_user_data(data):
errors = []
if not data.get('name'):
errors.append(ValueError("姓名不能为空"))
if not isinstance(data.get('age'), int) or data['age'] < 0:
errors.append(ValueError("年龄必须是正整数"))
if '@' not in data.get('email', ''):
errors.append(ValueError("邮箱格式不正确"))
if errors:
raise ExceptionGroup("数据验证失败", errors)
try:
validate_user_data({'name': '', 'age': -5, 'email': 'invalid'})
except* ValueError as eg:
for e in eg.exceptions:
print(f"验证错误: {e}")
BaseExceptionGroup 和 ExceptionGroup
BaseExceptionGroup:可以包含任何异常类型(包括KeyboardInterrupt、SystemExit)ExceptionGroup:只能包含Exception的子类(不包括KeyboardInterrupt等)
# ExceptionGroup 不能包含 KeyboardInterrupt
try:
raise ExceptionGroup("测试", [KeyboardInterrupt()])
except TypeError as e:
print(f"类型错误: {e}")
# ExceptionGroup 只能包含 Exception 实例
# 使用 BaseExceptionGroup
try:
raise BaseExceptionGroup("测试", [KeyboardInterrupt()])
except BaseExceptionGroup as eg:
print(f"捕获: {eg}")
异常组的操作
try:
raise ExceptionGroup("错误组", [
ValueError("错误1"),
TypeError("错误2"),
ValueError("错误3")
])
except ExceptionGroup as eg:
# 获取异常消息
print(f"消息: {eg.message}")
# 获取所有异常
print(f"异常数量: {len(eg.exceptions)}")
# 遍历异常
for e in eg.exceptions:
print(f" - {type(e).__name__}: {e}")
# 拆分异常组
value_errors, rest = eg.split(ValueError)
print(f"值错误数量: {len(value_errors.exceptions)}")
异常链与异常组的对比
| 特性 | 异常链 | 异常组 |
|---|---|---|
| 用途 | 表示因果关系 | 表示并行错误 |
| 结构 | 线性链 | 树形结构 |
| 语法 | raise ... from | ExceptionGroup(...) |
| 捕获 | 普通 except | except* |
| 引入版本 | Python 3 | Python 3.11 |
断言
使用 assert 进行调试断言:
def divide(a, b):
assert b != 0, "除数不能为零"
return a / b
print(divide(10, 2)) # 5.0
print(divide(10, 0)) # AssertionError: 除数不能为零
上下文管理器
with 语句
自动管理资源:
# 文件操作
with open("test.txt", "r") as file:
content = file.read()
# 文件在这里会自动关闭
# 锁操作
from threading import Lock
lock = Lock()
with lock:
# 临界区代码
print("获取了锁")
# 锁会自动释放
自定义上下文管理器
class MyContext:
def __enter__(self):
print("进入上下文")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("退出上下文")
if exc_type is not None:
print(f"发生异常:{exc_type.__name__}")
return False # 不阻止异常传播
with MyContext() as ctx:
print("在上下文中")
# 可以在这里抛出异常
# raise ValueError("测试异常")
使用 contextmanager 装饰器
from contextlib import contextmanager
@contextmanager
def my_context():
print("进入")
try:
yield "资源"
finally:
print("退出")
with my_context() as resource:
print(f"使用{resource}")
最佳实践
1. 尽量具体地捕获异常
# 不推荐
try:
result = some_function()
except:
pass
# 推荐
try:
result = some_function()
except SpecificException as e:
handle_error(e)
2. 不要过度使用异常
# 不推荐:使用异常控制流程
try:
result = data["key"]
except KeyError:
result = default_value
# 推荐:使用 dict 方法
result = data.get("key", default_value)
3. 记录异常日志
import logging
logging.basicConfig(level=logging.ERROR)
try:
result = some_function()
except Exception as e:
logging.error(f"发生错误:{e}", exc_info=True)
4. 清理资源
# 使用 try-finally
file = None
try:
file = open("test.txt", "r")
content = file.read()
finally:
if file:
file.close()
# 使用 with 语句(推荐)
with open("test.txt", "r") as file:
content = file.read()
小结
本章我们学习了:
- 常见异常类型:SyntaxError、ValueError、TypeError 等
- try-except 结构:捕获和处理异常
- else 和 finally 子句:处理正常情况和清理资源
- 主动抛出异常:使用
raise抛出异常 - 自定义异常类:创建应用特定的异常类型
- 异常链:
- 使用
raise ... from创建显式异常链 - 隐式异常链和
__context__ - 使用
raise ... from None禁止异常链
- 使用
- 异常组(Python 3.11+):
ExceptionGroup和BaseExceptionGroup- 使用
except*匹配异常组 - 并发任务和数据验证场景
- 断言:使用
assert进行调试检查 - 上下文管理器:
with语句和自定义上下文管理器 - 异常处理最佳实践
练习
- 编写一个除法计算器,处理各种异常
- 创建一个学生信息管理系统,验证输入数据的合法性
- 实现一个文件读取函数,处理各种文件相关异常
- 使用上下文管理器实现一个计时器