Python异常处理完全指南
Python异常处理完全指南
从入门到实战,学会写出健壮的Python代码
一、什么是异常?
异常(Exception)是程序运行时发生的错误事件。当Python解释器遇到无法执行的代码时,就会抛出异常,如果不对异常进行处理,程序就会崩溃退出。
一个最简单的例子
# 正常代码
print("程序开始")
num = int(input("请输入一个数字: "))
result = 10 / num
print(f"结果是: {result}")
print("程序结束")
# 如果用户输入 0,程序会崩溃:
# 请输入一个数字: 0
# ZeroDivisionError: division by zero
# 💥 程序直接终止,"程序结束"永远不会打印
异常处理的目的:让程序在出错时能够优雅地处理错误,而不是直接崩溃。
二、异常处理基础
2.1 try-except 基本结构
try:
# 可能抛出异常的代码
num = int(input("请输入一个数字: "))
result = 10 / num
print(f"结果是: {result}")
except ValueError:
# 处理数值转换错误
print("❌ 请输入有效的数字!")
except ZeroDivisionError:
# 处理除以零错误
print("❌ 除数不能为零!")
print("✅ 程序继续执行")
执行流程:
- 执行
try块中的代码 - 如果没有异常,跳过所有
except - 如果有异常,跳转到匹配的
except块 - 无论是否发生异常,都会继续执行后面的代码
2.2 捕获所有异常(谨慎使用)
try:
num = int(input("请输入数字: "))
result = 10 / num
except Exception as e:
# 捕获所有异常
print(f"发生错误: {type(e).__name__} - {e}")
# 更极端的情况(不推荐):
try:
risky_operation()
except: # 裸except会捕获所有异常,包括系统退出等
print("发生了错误")
⚠️ 警告:捕获所有异常时要小心,它会捕获
KeyboardInterrupt(Ctrl+C)等系统异常,可能导致程序无法正常退出。
2.3 获取异常详细信息
try:
with open("不存在的文件.txt", "r") as f:
content = f.read()
except FileNotFoundError as e:
print(f"错误类型: {type(e).__name__}")
print(f"错误信息: {e}")
print(f"错误参数: {e.args}")
# 输出:
# 错误类型: FileNotFoundError
# 错误信息: [Errno 2] No such file or directory: '不存在的文件.txt'
# 错误参数: (2, 'No such file or directory', '不存在的文件.txt')
三、完整的异常处理结构
3.1 try-except-else-finally
try:
# 主代码
file = open("data.txt", "r")
content = file.read()
except FileNotFoundError:
# 处理异常
print("文件不存在,创建新文件")
file = open("data.txt", "w")
file.write("默认内容")
else:
# 没有异常时执行
print(f"文件内容: {content[:100]}...")
finally:
# 无论是否发生异常都会执行
file.close()
print("文件已关闭")
各部分的执行时机:
| 部分 | 何时执行 | 用途 |
|---|---|---|
try |
总是先执行 | 放置可能出错的代码 |
except |
发生匹配的异常时 | 处理错误 |
else |
没有异常时 | 放置正常流程的代码 |
finally |
总是执行 | 释放资源(关闭文件、数据库连接等) |
3.2 实战示例:带重试机制的文件读取
import time
def read_file_with_retry(filename, max_retries=3):
"""带重试机制的文件读取"""
for attempt in range(1, max_retries + 1):
try:
with open(filename, 'r', encoding='utf-8') as f:
return f.read()
except FileNotFoundError:
print(f"尝试 {attempt}/{max_retries}: 文件不存在")
if attempt == max_retries:
raise # 重试耗尽,重新抛出异常
time.sleep(1) # 等待1秒后重试
except PermissionError:
print(f"尝试 {attempt}/{max_retries}: 权限不足")
if attempt == max_retries:
raise
time.sleep(1)
# 使用
try:
content = read_file_with_retry("important_data.txt")
print(content)
except FileNotFoundError:
print("❌ 文件不存在,请检查文件路径")
except PermissionError:
print("❌ 没有读取权限")
四、自定义异常
4.1 创建自定义异常类
# 定义自定义异常(继承自 Exception 或其子类)
class InsufficientBalanceError(Exception):
"""余额不足异常"""
pass
class InvalidAmountError(ValueError):
"""无效金额异常"""
pass
class BankAccount:
def __init__(self, balance=0):
self.balance = balance
def withdraw(self, amount):
if amount <= 0:
raise InvalidAmountError(f"提现金额必须大于0,当前: {amount}")
if amount > self.balance:
raise InsufficientBalanceError(
f"余额不足!当前余额: {self.balance}, 需要: {amount}"
)
self.balance -= amount
return self.balance
# 使用自定义异常
account = BankAccount(100)
try:
account.withdraw(50) # 正常
account.withdraw(100) # 抛出 InsufficientBalanceError
except InsufficientBalanceError as e:
print(f"取款失败: {e}")
except InvalidAmountError as e:
print(f"金额无效: {e}")
4.2 自定义异常的实用场景
class APIError(Exception):
"""API调用基础异常"""
pass
class APITimeoutError(APIError):
"""API超时异常"""
pass
class APIRateLimitError(APIError):
"""API限流异常"""
pass
class APIAuthenticationError(APIError):
"""API认证异常"""
pass
def call_api(endpoint):
# 模拟API调用
import random
rand = random.randint(1, 4)
if rand == 1:
raise APITimeoutError("请求超时")
elif rand == 2:
raise APIRateLimitError("请求过于频繁")
elif rand == 3:
raise APIAuthenticationError("Token无效")
else:
return {"status": "success"}
# 分层捕获
try:
result = call_api("/user/info")
print(result)
except APITimeoutError:
print("请稍后重试")
except APIRateLimitError:
print("请求太频繁,请等待")
except APIAuthenticationError:
print("请重新登录")
except APIError as e: # 捕获所有API相关异常
print(f"API调用失败: {e}")
五、异常处理的最佳实践
5.1 只捕获你预期会发生的异常
# ❌ 错误示例:捕获太宽泛
try:
data = json.loads(response)
user_id = data['user']['id']
except: # 会隐藏所有错误,包括拼写错误
print("处理失败")
# ✅ 正确示例:精确捕获
try:
data = json.loads(response)
user_id = data['user']['id']
except json.JSONDecodeError as e:
print(f"JSON解析失败: {e}")
except KeyError as e:
print(f"缺少必要字段: {e}")
except TypeError as e:
print(f"数据类型错误: {e}")
5.2 在合适的层级处理异常
# 底层函数:抛出异常,不处理
def fetch_user_data(user_id):
if user_id < 0:
raise ValueError("用户ID不能为负数")
# 实际API调用...
return {"id": user_id, "name": "张三"}
# 中层函数:可以转换异常
def get_user_profile(user_id):
try:
data = fetch_user_data(user_id)
return data
except ValueError as e:
# 转换为更具体的异常
raise TypeError(f"无效的用户ID格式: {e}")
# 顶层:处理异常
try:
profile = get_user_profile(-1)
except TypeError as e:
print(f"获取用户失败: {e}")
# 返回默认值
profile = {"id": -1, "name": "未知用户"}
5.3 使用异常链保留原始信息
try:
with open("config.json", "r") as f:
config = json.load(f)
except FileNotFoundError as e:
# 方式1:显式链接
raise RuntimeError("无法加载配置文件") from e
except json.JSONDecodeError as e:
# 方式2:自动链接(使用 raise ... from None 可以隐藏原始异常)
raise RuntimeError("配置文件格式错误") from None
5.4 上下文管理器中的异常处理
# 使用 contextlib 简化资源管理
from contextlib import contextmanager
@contextmanager
def safe_connection(db_url):
conn = None
try:
conn = create_connection(db_url)
yield conn
except ConnectionError:
print(f"连接失败: {db_url}")
raise
finally:
if conn:
conn.close()
print("连接已关闭")
# 使用
with safe_connection("mysql://localhost:3306") as conn:
result = conn.query("SELECT * FROM users")
六、常见异常类型速查表
| 异常类型 | 常见场景 | 示例 |
|---|---|---|
SyntaxError |
代码语法错误 | if x > 1 print(x) |
IndentationError |
缩进错误 | 混用空格和Tab |
NameError |
使用未定义的变量 | print(undeclared_var) |
TypeError |
操作类型不匹配 | "1" + 1 |
ValueError |
值不符合预期 | int("abc") |
IndexError |
列表索引越界 | list[100] |
KeyError |
字典键不存在 | dict["missing_key"] |
AttributeError |
对象无此属性 | "abc".length()(应该是len) |
FileNotFoundError |
文件不存在 | open("notexist.txt") |
ZeroDivisionError |
除以零 | 10 / 0 |
ImportError / ModuleNotFoundError |
导入模块失败 | import nonexistent |
TimeoutError |
操作超时 | 网络请求超时 |
ConnectionError |
连接失败 | 数据库/网络连接失败 |
七、综合实战:完整的RAG系统异常处理
结合你之前的RAG项目,这里展示如何为关键模块添加健壮的异常处理:
# rag_core_with_exceptions.py
import sys
import logging
from typing import Optional, List, Dict
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# 自定义异常类
class RAGError(Exception):
"""RAG系统基础异常"""
pass
class DocumentLoadError(RAGError):
"""文档加载异常"""
pass
class EmbeddingError(RAGError):
"""向量化异常"""
pass
class VectorStoreError(RAGError):
"""向量库操作异常"""
pass
class DocumentLoader:
"""带异常处理的文档加载器"""
@classmethod
def load_document(cls, file_path: str) -> Dict[str, str]:
try:
ext = file_path.split('.')[-1].lower()
if ext == 'pdf':
# 模拟PDF加载
if not file_path.endswith('.pdf'):
raise DocumentLoadError(f"文件格式错误: {file_path}")
return {"content": "PDF内容"}
elif ext == 'txt':
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
return {"content": content}
except FileNotFoundError:
raise DocumentLoadError(f"文件不存在: {file_path}")
except PermissionError:
raise DocumentLoadError(f"无权限读取: {file_path}")
except UnicodeDecodeError as e:
raise DocumentLoadError(f"文件编码错误: {e}")
else:
raise DocumentLoadError(f"不支持的文件格式: {ext}")
except DocumentLoadError:
# 直接重新抛出自定义异常
raise
except Exception as e:
# 捕获所有其他异常,转换为DocumentLoadError
logger.error(f"加载文档时发生未知错误: {e}")
raise DocumentLoadError(f"加载文档失败: {e}") from e
class RAGPipeline:
"""带完整异常处理的RAG流水线"""
def __init__(self, chunk_size: int = 500):
self.chunk_size = chunk_size
self.is_initialized = False
self.doc_count = 0
def ingest_documents(self, file_paths: List[str]) -> Dict[str, int]:
"""
摄入文档,返回统计信息
"""
stats = {
"total_files": len(file_paths),
"success_files": 0,
"failed_files": 0,
"chunks_created": 0,
"errors": []
}
loader = DocumentLoader()
for file_path in file_paths:
try:
logger.info(f"正在处理: {file_path}")
doc = loader.load_document(file_path)
self.doc_count += 1
stats["success_files"] += 1
stats["chunks_created"] += len(doc["content"]) // self.chunk_size + 1
except DocumentLoadError as e:
stats["failed_files"] += 1
stats["errors"].append(f"{file_path}: {e}")
logger.warning(f"跳过文件 {file_path}: {e}")
except Exception as e:
stats["failed_files"] += 1
stats["errors"].append(f"{file_path}: 未知错误 {e}")
logger.error(f"处理文件 {file_path} 时发生未知错误: {e}", exc_info=True)
self.is_initialized = stats["success_files"] > 0
logger.info(f"文档摄入完成: {stats}")
return stats
def query(self, question: str, top_k: int = 5) -> Dict:
"""
查询接口,带完整的异常处理
"""
# 验证状态
if not self.is_initialized:
raise VectorStoreError("RAG系统尚未初始化,请先摄入文档")
if not question or not question.strip():
raise ValueError("查询问题不能为空")
try:
# 模拟向量化
logger.info(f"处理查询: {question[:50]}...")
query_vector = self._embed_query(question)
# 模拟检索
results = self._search(query_vector, top_k)
# 模拟精排
final_results = self._rerank(question, results)
return {
"question": question,
"contexts": final_results,
"status": "success",
"total_found": len(final_results)
}
except EmbeddingError as e:
logger.error(f"向量化失败: {e}")
return {
"question": question,
"status": "error",
"error_type": "embedding_error",
"error_message": str(e)
}
except VectorStoreError as e:
logger.error(f"检索失败: {e}")
return {
"question": question,
"status": "error",
"error_type": "search_error",
"error_message": str(e)
}
except Exception as e:
logger.error(f"查询处理失败: {e}", exc_info=True)
return {
"question": question,
"status": "error",
"error_type": "unknown_error",
"error_message": f"系统错误: {e}"
}
def _embed_query(self, question: str):
"""模拟向量化,可能抛出EmbeddingError"""
try:
# 模拟Embedding模型出错
import random
if random.random() < 0.1: # 10%概率模拟失败
raise ConnectionError("Embedding服务连接超时")
return [0.1] * 768
except ConnectionError as e:
raise EmbeddingError(f"Embedding服务不可用: {e}") from e
def _search(self, vector, top_k: int):
"""模拟检索,可能抛出VectorStoreError"""
try:
# 模拟向量库检索
return [{"document": f"模拟文档{i}", "score": 0.9 - i*0.1} for i in range(top_k)]
except Exception as e:
raise VectorStoreError(f"向量检索失败: {e}") from e
def _rerank(self, question: str, candidates: List[Dict]) -> List[Dict]:
"""模拟精排"""
return candidates[:3]
# ---------- 使用示例 ----------
def main():
pipeline = RAGPipeline()
# 场景1:正常使用
print("=== 场景1: 正常使用 ===")
stats = pipeline.ingest_documents([
"data/doc1.txt",
"data/doc2.pdf"
])
print(f"摄入统计: {stats}")
# 场景2:查询
result = pipeline.query("什么是RAG?")
print(f"查询结果: {result}")
# 场景3:异常处理(未初始化)
print("\n=== 场景2: 未初始化查询 ===")
new_pipeline = RAGPipeline()
try:
result = new_pipeline.query("测试问题")
except VectorStoreError as e:
print(f"预期错误: {e}")
# 场景4:空查询
print("\n=== 场景3: 空查询 ===")
try:
result = pipeline.query("")
except ValueError as e:
print(f"预期错误: {e}")
# 场景5:批量处理,一个文件失败不影响其他
print("\n=== 场景4: 批量处理带错误 ===")
stats = pipeline.ingest_documents([
"data/exists.txt",
"data/notexist.txt", # 不存在
"data/another.txt",
])
print(f"成功: {stats['success_files']}, 失败: {stats['failed_files']}")
for error in stats['errors']:
print(f" - {error}")
if __name__ == "__main__":
main()
八、总结与速查
核心要点
- 使用
try-except捕获预期的异常 - 使用
finally释放资源(文件、数据库连接、锁等) - 创建自定义异常让代码意图更清晰
- 在合适的层级处理异常,底层抛出,顶层处理
- 记录异常日志,方便排查问题
- 不要捕获
Exception的裸形式,除非有充分理由
异常处理决策树
代码可能出错吗?
│
├── 否 → 正常执行
│
└── 是 → 能否修复?
│
├── 能(重试/降级)→ 立即处理
│
└── 不能 → 谁应该处理?
│
├── 当前函数能处理 → 捕获并处理
│
└── 调用方应该处理 → 抛出异常(使用合适的异常类型)
常用模板
# 模板1: 基本操作
try:
result = risky_operation()
except SpecificException as e:
handle_error(e)
# 模板2: 资源管理
try:
resource = acquire_resource()
use_resource(resource)
finally:
release_resource(resource)
# 模板3: 带上下文的资源管理(推荐)
with acquire_resource() as resource:
use_resource(resource) # 自动释放
# 模板4: 重试逻辑
for attempt in range(max_retries):
try:
return risky_operation()
except RetryableError:
if attempt == max_retries - 1:
raise
time.sleep(backoff(attempt))
# 模板5: 异常转换
try:
low_level_operation()
except LowLevelError as e:
raise HighLevelError("高级错误描述") from e
💡 记住:好的异常处理不是让程序永远不崩溃,而是让程序在出错时能够清晰地告诉你"发生了什么、在哪里、为什么",并且尽可能地恢复或优雅地退出。
有任何疑问,欢迎在评论区交流! 😄

浙公网安备 33010602011771号