Python 异常处理与特性

本章学习知识点

  • 异常捕获:try-except-else-finally、自定义异常
  • 调试技巧:print 调试、断点调试、日志模块(logging)

在 Python 开发中,代码运行时的错误(异常)和逻辑漏洞难以完全避免。异常处理能让程序在遇到错误时优雅降级而非直接崩溃,调试技巧则能快速定位并修复漏洞。

一、异常处理

异常是程序运行时发生的意外情况(如除零错误、文件不存在、类型不匹配等)。Python 提供了 try-except 系列语句捕获异常,并通过自定义异常实现业务场景的错误封装。

1.1、异常处理结构

try-except-else-finally: 这是 Python 异常处理的标准范式,四个关键字各司其职,形成完整的错误处理闭环。其执行逻辑为:尝试执行核心代码 → 捕获指定异常 → 无异常则执行额外逻辑 → 无论是否异常都执行收尾操作

  • 基础结构解析

    try:
        # 1. 尝试执行的核心业务代码(可能发生异常的代码)
        risky_operation()
    except 异常类型1 as e:
        # 2. 捕获到「异常类型1」时执行的处理逻辑
        handle_error1(e)  # e 为异常对象,包含错误信息
    except (异常类型2, 异常类型3) as e:
        # 3. 捕获多个异常(用元组包裹),统一处理
        handle_errors2_3(e)
    except Exception as e:
        # 4. 捕获所有其他未明确指定的异常(兜底,不建议滥用)
        handle_all_other_errors(e)
    else:
        # 5. 仅当 try 块无异常时才执行(可选)
        # 用于执行正常流程的后续操作
        normal_operation()
    finally:
        # 6. 无论是否发生异常,必定执行(可选)
        # 用于资源释放等收尾操作
        release_resource()
    
  • 关键组件作用与实战场景

    1. try 块:精准包裹风险代码

      • 作用:仅包裹「可能触发异常的核心代码」(如文件读写、网络请求、数据类型转换、数值计算等),无关代码不纳入,避免掩盖异常真实触发位置。

      • 示例: 仅将「文件打开 + 读取」放入 try,而非整个业务逻辑

        # 推荐写法
        def read_file(file_path):
            try:
                # 仅包裹可能抛异常的代码
                with open(file_path, "r", encoding="utf-8") as f:
                    return f.read()
            except FileNotFoundError:
                print(f"文件 {file_path} 不存在")
        
    2. except 块:精准捕获指定异常

      • 核心原则:避免用 except Exception: 兜底捕获,需按「异常类型精准匹配」,Python 常见核心异常如下:

        异常类型 含义 典型触发场景
        TypeError 数据类型不匹配 1 + “2”(整数与字符串拼接)
        ValueError 数据值无效 int (“abc”)(非数字字符串转整数)
        ZeroDivisionError 除零错误 10 / 0(除数为 0)
        FileNotFoundError 文件 / 路径不存在 open(“nonexistent.txt”)
        KeyError 字典键不存在 {“a”:1}[“b”]
        IndexError 序列索引越界 [1,2,3][10](列表下标超出范围)
      • 示例:按类型分别捕获,而非一刀切

        try:
            num = int(input("请输入数字:"))
            res = 10 / num
        except ValueError:
            print("输入的不是有效数字")
        except ZeroDivisionError:
            print("不能输入0作为除数")
        
    3. else 块:分离正常逻辑与异常处理

      • 作用:仅在 try 块无异常时执行「正常流程的后续操作」(如数据校验通过后写入数据库、计算结果格式化等),让正常逻辑与异常处理解耦,提升可读性。

      • 示例

        try:
            num = int(input("请输入数字:"))
        except ValueError:
            print("输入非数字")
        else:
            # 无异常时执行正常逻辑
            print(f"输入的数字是:{num},平方值为:{num**2}")
        
    4. finally 块:强制释放资源

      • 核心作用:执行「必须完成的资源释放操作」(如关闭文件句柄、断开数据库连接、释放锁、终止网络请求等)。

      • 关键特性:无论 try/except/else 块中是否有 returnbreak 等语句,finally 块都会优先执行,确保资源不泄漏。

      • 示例

        conn = None
        try:
            conn = get_db_connection()  # 建立数据库连接
            conn.execute("INSERT INTO t_user VALUES (1, 'test')")
        except DatabaseError:
            print("数据库操作失败")
        finally:
            # 无论是否异常,均关闭连接
            if conn:
                conn.close()
        
  • 实战:文件读写的完整异常处理

    • 文件读写涉及「文件不存在、权限不足、读写错误」等多种异常,用完整范式处理可确保程序稳健运行,并正确释放文件资源。

      def read_file(file_path):
          # 初始化文件句柄为 None
          file = None
          try:
              # 尝试打开文件并读取内容(核心操作)
              file = open(file_path, "r", encoding="utf-8")
              content = file.read()
          except FileNotFoundError as e:
              # 捕获「文件不存在」异常
              print(f"错误:文件 {file_path} 不存在 → {e}")
              return None
          except PermissionError as e:
              # 捕获「权限不足」异常
              print(f"错误:无权限读取文件 → {e}")
              return None
          except UnicodeDecodeError as e:
              # 捕获「编码解析」异常
              print(f"错误:文件编码不是 UTF-8 → {e}")
              return None
          else:
              # 无异常时执行:返回读取内容(正常流程)
              print(f"文件 {file_path} 读取成功,长度:{len(content)} 字节")
              return content
          finally:
              # 无论是否异常,都关闭文件(资源释放)
              if file:  # 确保文件句柄已创建
                  file.close()
                  print("文件句柄已关闭")
      
      # 测试:读取存在的文件
      read_file("test.txt")
      # 测试:读取不存在的文件
      read_file("nonexistent.txt")
      

      优化技巧:Python 提供 with 语句(上下文管理器),可自动管理文件、网络连接等资源,替代 finally 块的手动释放。上述代码可简化为 with open(...) as file: content = file.read(),无需手动关闭文件。

1.2、自定义异常

Python 内置异常适用于通用错误,但业务系统中需要更具体的错误描述(如「用户余额不足」「订单状态异常」)。通过自定义异常,可实现错误的分类管理和精准反馈。

  • 自定义异常实现规则

    1. 继承自 Python 内置的 Exception 类(不要继承 BaseException,它包含系统级异常如 KeyboardInterrupt);
    2. 通过 __init__ 方法自定义异常信息,可添加业务相关的额外属性(如错误码);
    3. raise 语句主动抛出自定义异常。
  • 实战:电商订单的自定义异常

    • 针对电商订单场景,定义「余额不足、订单不存在、状态异常」三种自定义异常,实现业务错误的精准捕获和处理。

      # 1. 定义自定义异常(继承自 Exception)
      class OrderError(Exception):
          """订单相关异常的基类(所有订单错误都继承此类)"""
          def __init__(self, error_code, message):
              self.error_code = error_code  # 业务错误码(便于前端处理)
              self.message = message        # 错误描述
              super().__init__(f"[{error_code}] {message}")  # 调用父类构造
      
      class InsufficientBalanceError(OrderError):
          """余额不足异常(继承自订单异常基类)"""
          def __init__(self, balance, need):
              # 错误码:1001,携带余额和所需金额信息
              super().__init__(1001, f"余额不足,当前余额:{balance},所需金额:{need}")
      
      class OrderNotFoundError(OrderError):
          """订单不存在异常"""
          def __init__(self, order_id):
              super().__init__(1002, f"订单 {order_id} 不存在")
      
      class OrderStatusError(OrderError):
          """订单状态异常(如已取消的订单再次支付)"""
          def __init__(self, order_id, current_status):
              super().__init__(1003, f"订单 {order_id} 状态异常,当前状态:{current_status}")
      
      # 2. 业务逻辑函数(主动抛出自定义异常)
      def create_order(user_id, order_id, amount):
          """创建订单:检查余额,生成订单"""
          # 模拟用户余额查询
          user_balance = 500  # 假设用户余额为 500
          # 模拟订单状态查询
          existing_orders = {"OD12345": "已支付"}  # 已存在的订单
          
          # 检查订单是否已存在
          if order_id in existing_orders:
              raise OrderStatusError(order_id, existing_orders[order_id])
          # 检查余额是否充足
          if user_balance < amount:
              raise InsufficientBalanceError(user_balance, amount)
          # 无异常则创建订单
          print(f"订单 {order_id} 创建成功,用户 {user_id} 支付金额:{amount}")
      
      # 3. 调用业务函数(捕获自定义异常)
      try:
          create_order(user_id=1001, order_id="OD12345", amount=600)
      except InsufficientBalanceError as e:
          print(f"创建订单失败(余额问题):{e.message},错误码:{e.error_code}")
      except OrderNotFoundError as e:
          print(f"创建订单失败(订单不存在):{e.message},错误码:{e.error_code}")
      except OrderStatusError as e:
          print(f"创建订单失败(状态问题):{e.message},错误码:{e.error_code}")
      except OrderError as e:
          # 捕获所有订单相关的其他异常(基类兜底)
          print(f"创建订单失败:{e.message},错误码:{e.error_code}")
      

      运行结果:创建订单失败(状态问题):订单 OD12345 状态异常,当前状态:已支付,错误码:1003

二、调试技巧

调试是排查代码逻辑错误、异常原因的过程。Python 提供了「简单打印调试、专业断点调试、日志记录调试」三种核心方式,分别适用于不同场景(开发初期、复杂逻辑、生产环境)。

2.1、打印调试

通过 print 语句输出变量值、代码执行流程,适用于 小型脚本、简单逻辑 的快速调试。优点是成本低、无需额外工具;缺点是调试后需删除打印语句,易遗漏。

  • 关键打印技巧

    • 打印变量类型和值:用 print(f"变量名: {var}, 类型: {type(var)}") 定位类型错误;
    • 标记执行流程:在分支、循环中打印 print("进入 if 分支"),确认代码执行路径;
    • 打印中间结果:在复杂计算中插入打印,如 print(f"步骤1结果: {res1}"),定位哪一步出现偏差。
  • 实战:打印调试定位排序逻辑错误

    def bubble_sort(nums):
        """冒泡排序函数(存在逻辑错误)"""
        n = len(nums)
        for i in range(n):
            # 打印当前轮次和待排序数组
            print(f"第 {i+1} 轮开始,数组:{nums}")
            for j in range(n - i):  # 错误:应为 range(n - i - 1)
                # 打印当前比较的索引和值
                print(f"  比较 j={j} ({nums[j]}) 和 j+1={j+1} ({nums[j+1]})")
                if nums[j] > nums[j+1]:
                    # 交换后打印数组
                    nums[j], nums[j+1] = nums[j+1], nums[j]
                    print(f"  交换后数组:{nums}")
        return nums
    
    # 测试排序
    test_nums = [3, 1, 4, 1, 5]
    sorted_nums = bubble_sort(test_nums)
    print("最终排序结果:", sorted_nums)
    

    通过打印输出可发现:最后一轮仍在比较已排序完成的元素,且可能出现 index out of range 错误,定位到内层循环条件应为 range(n - i - 1)

2.2、断点调试

断点调试是通过 IDE(如 PyCharm、VS Code)设置断点,让程序执行到断点处暂停,此时可逐行执行代码、查看变量实时值、观察调用栈,适用于 复杂逻辑、嵌套代码 的深度调试。

  • 流程(以 VS Code 为例)

    • 设置断点:点击代码行号左侧,出现红色圆点(断点),通常设置在「可能出错的代码行」或「逻辑分支入口」;
    • 启动调试:点击 IDE 左侧「运行和调试」图标,选择「Python 文件」启动调试,程序会执行到第一个断点处暂停;
    • 逐行执行: 「单步跳过(F10)」:执行当前行,不进入函数内部;
      • 「单步调试(F11)」:执行当前行,若为函数则进入函数内部;
      • 「继续(F5)」:从当前断点继续执行到下一个断点;
      • 「跳出(Shift+F11)」:从当前函数内部跳出。
    • 查看状态:调试面板可查看「变量」(实时值)、「调用栈」(函数调用层级)、「断点列表」(管理所有断点)。
  • 实战:断点调试定位函数返回值错误

    • 假设以下代码计算阶乘时返回值错误,用断点调试定位问题:

      def factorial(n):
          """计算 n 的阶乘(存在逻辑错误)"""
          result = 1
          if n < 0:
              return "输入不能为负数"
          while n > 0:
              result *= n
              n += 1  # 错误:应为 n -= 1
          return result
      
      # 测试阶乘
      print(factorial(5))  # 预期输出 120,实际进入死循环
      
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐