第十二章: 异常处理 异常处理是 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 ("请输入一个数字: " )) result = 10 / num 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: print (f"输入错误: {e} " ) except ZeroDivisionError as e: print (f"除零错误: {e} " ) except Exception as e: print (f"其他错误: {e} " ) else : print ("计算成功完成!" ) finally : print ("异常处理结束" )
12.2.2 各代码块执行条件总结 代码块 执行条件 典型用途 try
必定执行 放置可能出错的代码 except
对应类型异常发生时 处理特定类型错误 else
try 块无异常发生时 执行成功后的操作 finally
无论有无异常均执行 资源清理、释放
12.3 自定义异常 虽然 Python 提供了丰富的内置异常,但在开发特定应用时,创建自定义异常可以使代码更具可读性和针对性。
12.3.1 创建自定义异常类 1 2 3 4 5 6 7 8 9 10 11 12 13 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 def func_middle (): return func_inner() def func_outer (): try : return func_middle() except ZeroDivisionError: print ("捕获了除零错误!" ) return None result = func_outer()
12.4.2 重新抛出异常 重新抛出异常有两种方式:
直接使用 raise
语句不带参数 使用 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 ]) 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!' )
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 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} " ) 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 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 : 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 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() 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 def api_get_user (request, user_id ): try : profile = get_user_profile(user_id) return JSONResponse(status_code=200 , content=profile) except UserNotFoundError: 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 functoolsimport timedef 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 : 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 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]) try : process_multiple_tasks() except * ZeroDivisionError as eg: print (f"除零错误: {eg.exceptions} " ) except * TypeError as eg: print (f"类型错误: {eg.exceptions} " ) 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 if 'key' in my_dict and my_dict['key' ] is not None : value = my_dict['key' ] else : value = 'default' try : value = my_dict['key' ] except (KeyError, TypeError): value = 'default'