跳到主要内容

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:可以包含任何异常类型(包括 KeyboardInterruptSystemExit
  • 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 ... fromExceptionGroup(...)
捕获普通 exceptexcept*
引入版本Python 3Python 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()

小结

本章我们学习了:

  1. 常见异常类型:SyntaxError、ValueError、TypeError 等
  2. try-except 结构:捕获和处理异常
  3. else 和 finally 子句:处理正常情况和清理资源
  4. 主动抛出异常:使用 raise 抛出异常
  5. 自定义异常类:创建应用特定的异常类型
  6. 异常链
    • 使用 raise ... from 创建显式异常链
    • 隐式异常链和 __context__
    • 使用 raise ... from None 禁止异常链
  7. 异常组(Python 3.11+)
    • ExceptionGroupBaseExceptionGroup
    • 使用 except* 匹配异常组
    • 并发任务和数据验证场景
  8. 断言:使用 assert 进行调试检查
  9. 上下文管理器with 语句和自定义上下文管理器
  10. 异常处理最佳实践

练习

  1. 编写一个除法计算器,处理各种异常
  2. 创建一个学生信息管理系统,验证输入数据的合法性
  3. 实现一个文件读取函数,处理各种文件相关异常
  4. 使用上下文管理器实现一个计时器