Python(十三):第十二章: 异常处理

第十二章: 异常处理

异常处理是 Python 编程中的重要环节,它允许程序在遇到错误时优雅地恢复或退出,而不是直接崩溃。

12.1 基本异常处理

异常处理的核心是 try-except 结构,它允许程序捕获并处理运行时错误。

12.1.1 异常的概念与意义

异常是程序运行时发生的错误,会打断正常的程序执行流程。Python 提供了强大的异常处理机制,使程序能够:

  • 预测可能的错误并妥善处理
  • 提供用户友好的错误信息
  • 防止程序意外终止
  • 实现优雅的错误恢复策略

12.1.2 基础 try-except 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try:
# 可能引发异常的代码
num = int(input("请输入一个数字: ")) # 可能引发 ValueError
result = 10 / num # 可能引发 ZeroDivisionError
print(f"结果是: {result}")
except ValueError:
# 处理特定异常
print("输入必须是数字!")
except ZeroDivisionError:
# 处理另一种特定异常
print("不能除以零!")
except:
# 捕获所有其他异常(不推荐这种写法)
print("发生了其他错误!")
常见内置异常触发场景示例
ValueError传入无效值int("abc")
TypeError类型不匹配"2" + 2
ZeroDivisionError除数为零10/0
IndexError索引超出范围[1,2,3][10]
KeyError字典中不存在的键{"a":1}["b"]
FileNotFoundError文件不存在open("不存在.txt")
ImportError导入模块失败import 不存在模块
AttributeError对象没有特定属性"hello".append(1)

12.2 完整的异常处理结构

完整的异常处理结构包括 try, except, else, 和 finally 四个部分,每个部分负责不同的功能。

12.2.1 try-except-else-finally 完整结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try:
# 可能引发异常的代码
num = int(input("请输入一个数字: "))
result = 10 / num
print(f"结果是: {result}")
except ValueError as e:
# 处理特定异常,e 包含异常的详细信息
print(f"输入错误: {e}") # e 可能显示: "invalid literal for int() with base 10: 'abc'"
except ZeroDivisionError as e:
# 处理另一种特定异常
print(f"除零错误: {e}") # e 可能显示: "division by zero"
except Exception as e:
# 处理所有其他异常(这种方式比空except更好)
print(f"其他错误: {e}")
else:
# 只有当try块中的代码执行成功且没有异常发生时执行
print("计算成功完成!")
finally:
# 无论是否有异常都会执行的代码块
print("异常处理结束")

12.2.2 各代码块执行条件总结

代码块执行条件典型用途
try必定执行放置可能出错的代码
except对应类型异常发生时处理特定类型错误
elsetry 块无异常发生时执行成功后的操作
finally无论有无异常均执行资源清理、释放

12.3 自定义异常

虽然 Python 提供了丰富的内置异常,但在开发特定应用时,创建自定义异常可以使代码更具可读性和针对性。

12.3.1 创建自定义异常类

1
2
3
4
5
6
7
8
9
10
11
12
13
# 自定义异常类,继承自 Exception
class InsufficientFundsError(Exception):
"""当账户余额不足时引发的异常"""

def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
# 创建有意义的错误消息
self.message = f"余额不足: 当前余额 {balance} 元,尝试提取 {amount} 元"
super().__init__(self.message)

def __str__(self):
return self.message

12.3.2 使用自定义异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 在业务逻辑中使用自定义异常
class BankAccount:
def __init__(self, balance=0):
self.balance = balance

def withdraw(self, amount):
if amount > self.balance:
# 在适当的条件下抛出自定义异常
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
return amount

# 处理自定义异常的实际场景
account = BankAccount(100)
try:
# 尝试提取超过余额的金额
account.withdraw(150)
except InsufficientFundsError as e:
# 针对性地处理特定业务异常
print(f"操作失败: {e}")
print(f"您需要再存入至少 {e.amount - e.balance} 元")
# 可以在这里提供补救措施,比如自动转入资金或提供贷款选项
自定义异常命名惯例示例适用场景
以 “Error” 结尾ValidationError程序错误,需纠正
以 “Warning” 结尾DeprecationWarning警告级别的问题
以具体领域开头DatabaseConnectionError特定领域的异常

12.4 异常的传播与重新抛出

了解异常如何在调用栈中传播以及如何重新抛出异常对于构建稳健的错误处理系统至关重要。

12.4.1 异常传播机制

当异常发生时,Python 会沿着调用栈向上查找,直到找到相应的 except 子句处理该异常,如果没有处理程序,程序将终止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 异常传播示例
def func_inner():
# 这里的异常会向上传播
return 10 / 0 # 引发 ZeroDivisionError

def func_middle():
# 没有处理异常,所以异常继续传播
return func_inner()

def func_outer():
try:
# 在这里捕获来自更深层次函数的异常
return func_middle()
except ZeroDivisionError:
print("捕获了除零错误!")
return None

# 调用最外层函数
result = func_outer() # 输出: 捕获了除零错误!

12.4.2 重新抛出异常

重新抛出异常有两种方式:

  1. 直接使用 raise 语句不带参数
  2. 使用 raise ... from ... 结构表明异常的因果关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def process_data(data):
try:
# 尝试处理数据
result = data[0] / data[1]
return result
except ZeroDivisionError:
# 记录错误并重新抛出当前异常
print("除数不能为零!")
raise # 直接重新抛出当前捕获的异常
except IndexError as e:
# 捕获后转换为更具体的应用级异常,并保留原始错误信息
raise ValueError("数据格式不正确,需要至少两个元素") from e

# 调用函数并处理异常
try:
# 尝试处理带有问题的数据
result = process_data([10]) # 数组只有一个元素,会引发 IndexError
except ValueError as e:
# 处理转换后的异常
print(f"发生错误: {e}")
# 访问原始异常
if e.__cause__:
print(f"原始错误: {e.__cause__}")
重新抛出方式语法适用场景
简单重抛raise仅记录错误后继续传播
转换异常raise NewError() from original_error将低级异常转换为应用级异常
清除上下文raise NewError() from None隐藏原始异常(不推荐)

12.5 使用上下文管理器

上下文管理器是 Python 的一种强大机制,通过 with 语句实现自动资源管理,特别适合处理需要显式打开和关闭的资源。

12.5.1 with 语句和资源管理

1
2
3
4
5
6
# 文件操作 - 最常见的上下文管理器应用场景
with open('file.txt', 'w') as f:
f.write('Hello, World!')
# 可能发生异常的代码
# raise ValueError("演示异常")
# 即使发生异常,文件也会自动关闭

12.5.2 自定义上下文管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class DatabaseConnection:
def __init__(self, connection_string):
self.connection_string = connection_string
self.connection = None

def __enter__(self):
"""进入上下文时调用,返回值被赋给as后的变量"""
print(f"连接到数据库: {self.connection_string}")
# 在实际应用中,这里会创建真正的数据库连接
self.connection = "已连接"
return self.connection

def __exit__(self, exc_type, exc_val, exc_tb):
"""离开上下文时调用,无论是正常退出还是异常退出
参数: 异常类型、异常值、异常回溯信息"""
print("关闭数据库连接")
# 释放资源
self.connection = None

# 返回值决定异常处理:
# - True: 表示异常已处理,不再传播
# - False/None: 表示需要继续传播异常
return False # 让异常继续传播

12.5.3 实际应用场景

1
2
3
4
5
6
7
8
9
10
11
# 使用自定义上下文管理器进行数据库操作
try:
with DatabaseConnection("mysql://localhost/mydb") as conn:
print(f"使用连接: {conn}")
# 数据库操作代码
# 模拟操作失败
# raise ValueError("数据插入失败")
except Exception as e:
print(f"捕获到异常: {e}")
# 处理数据库操作异常
# 可能的恢复策略: 重试、记录日志、发送报警等
常见上下文管理器示例自动管理的资源
open()with open('file.txt') as f:文件句柄
threading.Lock()with lock:线程锁
contextlib.suppress()with suppress(FileNotFoundError):忽略特定异常
tempfile.NamedTemporaryFile()with NamedTemporaryFile() as tmp:临时文件
requests.Session()with Session() as session:HTTP 会话

12.5.4 使用 contextlib 简化上下文管理器创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from contextlib import contextmanager

@contextmanager
def file_manager(filename, mode):
"""一个使用生成器函数创建的上下文管理器"""
try:
# 设置阶段 - 获取资源
f = open(filename, mode)
print(f"文件 {filename} 已打开")
# yield 语句将控制权传递给 with 块内的代码
yield f
finally:
# 清理阶段 - 释放资源
f.close()
print(f"文件 {filename} 已关闭")

# 使用自定义上下文管理器
with file_manager('example.txt', 'w') as file:
file.write('这是一个使用contextlib创建的上下文管理器示例')

12.6 异常处理最佳实践

掌握异常处理的模式和反模式对于编写健壮的代码至关重要。

12.6.1 不良实践与改进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 不好的做法:过于宽泛的异常捕获
def bad_practice():
try:
# 大量不同类型的操作混在一起
config = open("config.ini").read()
settings = parse_config(config)
result = process_data(settings)
save_result(result)
except:
# 捕获所有异常,无法区分不同错误
print("出错了")
# 无法提供有价值的错误信息
# 无法针对性恢复

# 好的做法:精确捕获和处理异常
def good_practice():
config = None
try:
# 只包含读取配置文件的代码
config = open("config.ini")
config_text = config.read()
except FileNotFoundError:
# 针对性处理配置文件缺失
print("配置文件不存在,将使用默认配置")
config_text = DEFAULT_CONFIG
except PermissionError:
# 针对性处理权限问题
print("没有读取配置文件的权限")
# 可以请求提升权限或使用备用方案
return None
finally:
# 确保文件被关闭
if config:
config.close()

try:
# 解析配置的代码单独放在try块中
settings = parse_config(config_text)
except ValueError as e:
# 处理配置格式错误
print(f"配置格式错误: {e}")
return None

# 后续操作...

12.6.2 实际开发中的异常处理策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 分层异常处理示例 - Web应用请求处理

# 1. 底层数据访问层: 转换为应用层可理解的异常
def fetch_user_data(user_id):
try:
# 数据库操作
connection = get_db_connection()
cursor = connection.cursor()
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
result = cursor.fetchone()
return result
except MySQLError as e:
# 转换为应用级异常
if e.errno == 1045: # 访问被拒绝
raise DatabaseAccessError("数据库访问被拒绝") from e
elif e.errno == 2003: # 连接失败
raise DatabaseConnectionError("无法连接到数据库") from e
else:
raise DatabaseError(f"数据库错误: {e}") from e
finally:
# 资源清理
cursor.close()
connection.close()

# 2. 业务逻辑层: 处理应用级异常
def get_user_profile(user_id):
try:
user_data = fetch_user_data(user_id)
if not user_data:
# 应用逻辑异常
raise UserNotFoundError(f"用户ID {user_id} 不存在")
return format_user_profile(user_data)
except DatabaseError as e:
# 日志记录并决定是否传播
logger.error(f"获取用户数据失败: {e}")
# 可能的重试策略
if isinstance(e, DatabaseConnectionError) and retry_count < MAX_RETRIES:
return get_user_profile_with_retry(user_id, retry_count + 1)
# 传播异常供上层处理
raise

# 3. 接口层: 向用户展示友好错误
def api_get_user(request, user_id):
try:
profile = get_user_profile(user_id)
return JSONResponse(status_code=200, content=profile)
except UserNotFoundError:
# 返回适当的HTTP状态码
return JSONResponse(status_code=404, content={"error": "用户不存在"})
except DatabaseConnectionError:
# 返回服务暂时不可用
return JSONResponse(status_code=503, content={"error": "服务暂时不可用"})
except Exception as e:
# 意外错误: 记录并返回通用错误
logger.critical(f"未处理的错误: {e}", exc_info=True)
return JSONResponse(status_code=500, content={"error": "服务器内部错误"})

12.7 高级异常处理技术

12.7.1 使用装饰器简化异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import functools
import time

def retry(max_attempts=3, delay=1):
"""一个用于自动重试的装饰器

参数:
max_attempts: 最大尝试次数
delay: 重试之间的延迟(秒)
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
attempts = 0
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except (ConnectionError, TimeoutError) as e:
attempts += 1
if attempts >= max_attempts:
raise # 达到最大尝试次数,重新抛出异常
print(f"操作失败: {e}{delay}秒后重试 ({attempts}/{max_attempts})")
time.sleep(delay)
return None # 不会执行到这里
return wrapper
return decorator

# 使用重试装饰器
@retry(max_attempts=3, delay=2)
def connect_to_server(url):
"""连接到远程服务器,可能会失败"""
import random
if random.random() < 0.7: # 模拟70%的失败率
raise ConnectionError("连接服务器失败")
return "连接成功"

# 调用带重试功能的函数
try:
result = connect_to_server("https://example.com")
print(f"结果: {result}")
except ConnectionError:
print("连接服务器最终失败")

12.7.2 异常链与异常组

Python 3.10+ 引入了异常组(ExceptionGroup)和 except*语法,用于处理多个异常同时存在的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from typing import List


# Python 3.10+ 特性:异常组
def process_multiple_tasks():
# 用于收集任务处理过程中的错误
exceptions: List[tuple[str, Exception]] = []

tasks = [("task1", 0), ("task2", 2), ("task3", "not_a_number")]

for task_name, value in tasks:
try:
# 尝试处理任务
print(f"处理任务 {task_name},输入值为 {value}")
result = 10 / value
print(f"任务 {task_name} 处理结果为 {result}")
except Exception as e:
exceptions.append((task_name, e))
# 如果有错误,以异常组的形式抛出
if exceptions:
raise ExceptionGroup("处理任务过程中发生错误",
[ValueError(f"任务 {name} 处理失败:{err}") for name, err in exceptions])


# 使用except*处理异常组
try:
process_multiple_tasks()
except* ZeroDivisionError as eg:
# 处理所有除零错误
print(f"除零错误: {eg.exceptions}") # 这里就可以抓到task1的异常
except* TypeError as eg:
# 处理所有类型错误
print(f"类型错误: {eg.exceptions}") # 这里就可以抓到task3的异常
except* Exception as eg:
# 处理其他所有错误
print(f"其他错误: {eg.exceptions}")

12.7.3 EAFP vs LBYL 编程风格

Python 通常推崇 EAFP(“Easier to Ask Forgiveness than Permission”)而非 LBYL(“Look Before You Leap”):

1
2
3
4
5
6
7
8
9
10
11
# LBYL风格(先检查后操作)
if 'key' in my_dict and my_dict['key'] is not None:
value = my_dict['key']
else:
value = 'default'

# EAFP风格(先操作后处理异常)
try:
value = my_dict['key']
except (KeyError, TypeError):
value = 'default'