Python 错误与异常处理深度解析

目录

  1. 错误与异常概述
    • 1.1 语法错误 vs 异常
    • 1.2 异常的层次结构
  2. 基本异常处理:try/except
    • 2.1 捕获特定异常
    • 2.2 捕获多个异常
    • 2.3 获取异常对象
  3. 完整的异常处理结构
    • 3.1 else 子句
    • 3.2 finally 子句
    • 3.3 执行顺序与优先级
  4. 抛出异常:raise
    • 4.1 基本使用
    • 4.2 异常链与显式上下文
  5. 自定义异常
    • 5.1 创建异常类
    • 5.2 异常携带附加信息
  6. 高级技巧
    • 6.1 异常日志与 traceback
    • 6.2 断言 assert
    • 6.3 异常抑制与上下文管理器
  7. 上下文管理器与异常安全
    • 7.1 with 语句协议
    • 7.2 contextlib 工具
  8. 常见陷阱与最佳实践
  9. 总结

1. 错误与异常概述

1.1 语法错误 vs 异常

Python 中的错误至少可以分为两类:语法错误(SyntaxError)和异常(Exception)。

  • 语法错误在解析源代码阶段发生,程序根本无法执行。
    while True print('Hello')  # SyntaxError: invalid syntax
    
  • 异常是指在语法正确的前提下,运行时发生的错误。程序会立即终止并打印回溯信息,除非被捕获。
    10 / 0                     # ZeroDivisionError
    

1.2 异常的层次结构

所有内置异常都派生自 BaseException,常用的几支继承关系如下:

BaseException
 ├── SystemExit
 ├── KeyboardInterrupt
 └── Exception
      ├── ArithmeticError
      │    ├── ZeroDivisionError
      │    └── OverflowError
      ├── LookupError
      │    ├── IndexError
      │    └── KeyError
      ├── OSError
      ├── ValueError
      ├── TypeError
      └── ...

通常只需捕获 Exception(不包括 SystemExitKeyboardInterrupt),以免意外阻止程序退出或用户中断。


2. 基本异常处理:try/except

2.1 捕获特定异常

def safe_divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("除数不能为零!")
        result = None
    return result

print(safe_divide(10, 2))   # 5.0
print(safe_divide(10, 0))   # 除数不能为零!  None

2.2 捕获多个异常

可以将多个异常类型组成元组放在 except 后面,或使用多个 except 分支。

def get_item(data, index):
    try:
        return data[index]
    except (IndexError, TypeError) as e:
        print(f"访问失败:{e}")

get_item([1,2], 5)       # 访问失败:list index out of range
get_item("hello", "a")   # 访问失败:string indices must be integers

注意:捕获范围较窄的异常要放在前面,因为 except 是从上到下匹配的。

2.3 获取异常对象

使用 as 将异常实例绑定到变量,方便后续处理或日志记录。

try:
    num = int("abc")
except ValueError as e:
    print(f"转换失败: {e}")       # 转换失败: invalid literal for int() with base 10: 'abc'

3. 完整的异常处理结构

3.1 else 子句

如果 try 块没有发生异常,则执行 else 块。这有助于将可能触发异常的代码与希望仅在成功时执行的逻辑分开。

def read_config(filepath):
    config = {}
    try:
        with open(filepath) as f:
            raw = f.read()
    except FileNotFoundError:
        print(f"文件 {filepath} 不存在")
    else:
        config = eval(raw)         # 仅当文件读取成功后才解析
        print("配置加载成功")
    return config

else 块中出现的异常不会被前面的 except 捕获,这在逻辑上是合理的。

3.2 finally 子句

无论是否发生异常、异常是否被捕获,finally总是执行,常用于清理资源。

def process_file(path):
    file = None
    try:
        file = open(path)
        # 处理文件内容...
    except OSError:
        print("文件操作异常")
    finally:
        if file:
            file.close()
            print("文件已关闭")

即使 tryexcept 中有 return/break/continuefinally 也会在离开 try 前执行。

3.3 执行顺序与优先级

完整结构:try -> except (one or more) -> else -> finally

  • 只有 try 块未触发异常时,才执行 else
  • finally 一定会执行;如果 finally 中包含 return,会覆盖 try/except/else 中的返回值。
def demo():
    try:
        return "try"
    finally:
        return "finally"

print(demo())   # 输出 "finally"

4. 抛出异常:raise

4.1 基本使用

raise 用于主动触发异常,可携带描述信息。

def set_age(age):
    if not isinstance(age, int):
        raise TypeError("年龄必须是整数")
    if age < 0:
        raise ValueError("年龄不能为负")
    return age

# set_age("x")  引发 TypeError
# set_age(-5)   引发 ValueError

可以重新抛出当前正在处理的异常:

try:
    1 / 0
except ZeroDivisionError:
    print("记录日志后重新抛出")
    raise   # 将原异常继续向上传播

4.2 异常链与显式上下文

except 块中引发新的异常时,Python 3 会自动将原异常附加到新异常的 __context__ 属性上,形成异常链(During handling of the above exception, another exception occurred)。

try:
    int("abc")
except ValueError as e:
    raise RuntimeError("数据格式异常") from e   # 显式使用 from 表明因果关系

使用 from None 可以抑制上下文:

try:
    int("abc")
except ValueError:
    raise RuntimeError("数据无效") from None   # 不会有 "During handling..."

5. 自定义异常

5.1 创建异常类

自定义异常应继承 Exception(或其子类),保持命名以 Error 结尾(惯例)。

class InsufficientFundsError(Exception):
    """余额不足异常"""
    pass

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(f"余额 {balance} 不足,无法取出 {amount}")
    return balance - amount

5.2 异常携带附加信息

可以在自定义异常中重写 __init__ 保存更多上下文。

class APIError(Exception):
    def __init__(self, message, status_code=None, response=None):
        super().__init__(message)
        self.status_code = status_code
        self.response = response

try:
    raise APIError("Not Found", 404, "无此资源")
except APIError as e:
    print(f"HTTP {e.status_code}: {e}")

6. 高级技巧

6.1 异常日志与 traceback

使用 logging 模块记录异常信息,同时打印完整回溯。

import logging
import traceback

logging.basicConfig(level=logging.ERROR)

def dangerous():
    1 / 0

try:
    dangerous()
except Exception:
    logging.error("发生未预料的异常", exc_info=True)  # 打印 traceback
    # 或者手动获取
    tb = traceback.format_exc()
    print(tb)

6.2 断言 assert

assert 用于调试时强制检查某个条件,如果为 False 则抛出 AssertionError。不建议用于正式的错误处理,因为断言可能被 -O 优化标志禁用。

def sqrt(x):
    assert x >= 0, "不能对负数开平方根"
    return x ** 0.5

sqrt(-1)    # AssertionError: 不能对负数开平方根

6.3 异常抑制与上下文管理器

有时我们需要忽略特定的异常,可以用上下文管理器简洁地实现。

from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove("not_exist.file")    # 文件不存在也不会崩

7. 上下文管理器与异常安全

7.1 with 语句协议

实现 __enter____exit__ 的对象可以用 with 保证资源分配与释放。__exit__ 会接收异常信息,如果返回 True 则可抑制异常。

class Transaction:
    def __enter__(self):
        print("开始事务")
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            print("提交事务")
        else:
            print(f"回滚事务,因为 {exc_type.__name__}: {exc_val}")
        return False   # 不抑制异常

with Transaction() as tx:
    print("执行操作")
    # raise ValueError("故意出错")   # 会触发回滚

7.2 contextlib 工具

contextlib.contextmanager 装饰器可以将生成器函数转换为上下文管理器,更方便处理异常。

from contextlib import contextmanager

@contextmanager
def managed_resource(filename):
    f = open(filename)
    try:
        yield f
    except Exception:
        print("发生异常,做清理...")
    finally:
        f.close()

with managed_resource("data.txt") as file:
    content = file.read()

8. 常见陷阱与最佳实践

  • 避免裸 except::这会捕获 SystemExitKeyboardInterrupt,导致程序无法正常退出。 至少使用 except Exception:
  • 不要过度捕获异常:只处理你预期、知道如何处理的异常,其余向上层传播。
  • 记录但不要隐藏except: pass 可能将 bug 完全隐藏,难以调试。至少记录日志。
  • finally 并非绝对安全:如果 finally 块中又引发异常,会覆盖原先的异常。尽量保持 finally 简洁。
  • 使用 else 明确意图:把仅当成功才运行的代码放入 else,降低被意外捕获的风险。
  • 合理自定义异常:给特定领域添加有意义的异常类型,方便上层按类型处理。
  • 慎用断言:断言可能被跳过,不应依赖它做参数校验或业务逻辑。
  • 利用异常链:在重新抛出时 raise NewError(...) from original_exception,保留上下文以便调试。

9. 总结

异常是 Python 控制流中不可或缺的部分。通过合理的 try/except/else/finally 结构,我们可以让程序在面对错误时依然可控,同时保持资源的正确释放。自定义异常与异常链则让错误传递具有更强表现力。
结合上下文管理器,Python 提供了一套优雅的“申请-使用-无论成功失败都清理”的范式。理解这些工具,并在实际代码中遵循最佳实践,能显著提升程序的健壮性与可维护性。

Logo

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

更多推荐