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("✅ 程序继续执行")

执行流程

  1. 执行 try 块中的代码
  2. 如果没有异常,跳过所有 except
  3. 如果有异常,跳转到匹配的 except
  4. 无论是否发生异常,都会继续执行后面的代码

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()

八、总结与速查

核心要点

  1. 使用 try-except 捕获预期的异常
  2. 使用 finally 释放资源(文件、数据库连接、锁等)
  3. 创建自定义异常让代码意图更清晰
  4. 在合适的层级处理异常,底层抛出,顶层处理
  5. 记录异常日志,方便排查问题
  6. 不要捕获 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

💡 记住:好的异常处理不是让程序永远不崩溃,而是让程序在出错时能够清晰地告诉你"发生了什么、在哪里、为什么",并且尽可能地恢复或优雅地退出。

有任何疑问,欢迎在评论区交流! 😄

posted @ 2026-07-16 20:39  yuangu  阅读(21)  评论(0)    收藏  举报