Wiki LogoWiki - The Power of Many

Week 05: 异常处理与上下文管理

掌握 Python 异常机制、自定义异常、上下文管理器与 contextlib 模块.

1. 异常基础

1.1 什么是异常

异常是程序运行时发生的错误, 会中断正常的程序流程.

# ZeroDivisionError
result = 10 / 0

# KeyError
d = {}
d["key"]

# IndexError
lst = [1, 2, 3]
lst[10]

# TypeError
"hello" + 5

# ValueError
int("abc")

1.2 try/except 基本语法

try:
    result = 10 / 0
except ZeroDivisionError:
    print("除数不能为零")

1.3 捕获多个异常

try:
    result = int(input("Enter a number: "))
except ValueError:
    print("输入不是有效数字")
except KeyboardInterrupt:
    print("用户中断")

# 或者一起捕获
try:
    result = int(input("Enter a number: "))
except (ValueError, TypeError):
    print("输入无效")

1.4 获取异常信息

try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"错误类型: {type(e).__name__}")
    print(f"错误信息: {e}")

1.5 else 与 finally

try:
    f = open("file.txt")
except FileNotFoundError:
    print("文件不存在")
else:
    # try 成功时执行
    content = f.read()
    f.close()
finally:
    # 无论成功与否都执行
    print("清理操作")

finally 的保证:

def example():
    try:
        return "try"
    finally:
        print("finally 仍然执行")

result = example()
# finally 仍然执行
print(result)  # try

2. 异常层次结构

BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
    ├── StopIteration
    ├── ArithmeticError
    │   ├── ZeroDivisionError
    │   ├── FloatingPointError
    │   └── OverflowError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    ├── OSError
    │   ├── FileNotFoundError
    │   ├── PermissionError
    │   └── TimeoutError
    ├── ValueError
    ├── TypeError
    ├── AttributeError
    └── ...

2.1 Exception vs BaseException

  • 捕获 Exception: 捕获大多数异常
  • 不要捕获 BaseException: 会阻止 KeyboardInterruptSystemExit
# 推荐
try:
    risky_operation()
except Exception as e:
    handle_error(e)

# 不推荐
try:
    risky_operation()
except:  # 捕获所有, 包括 KeyboardInterrupt
    pass

3. 抛出异常

3.1 raise 语句

def divide(a, b):
    if b == 0:
        raise ValueError("除数不能为零")
    return a / b

try:
    divide(10, 0)
except ValueError as e:
    print(e)

3.2 重新抛出异常

try:
    result = 10 / 0
except ZeroDivisionError:
    print("记录日志")
    raise  # 重新抛出原异常

3.3 异常链 (Exception Chaining)

try:
    result = 10 / 0
except ZeroDivisionError as e:
    raise RuntimeError("计算失败") from e

# RuntimeError: 计算失败
# 
# The above exception was the direct cause of the following exception:
# 
# ZeroDivisionError: division by zero

4. 自定义异常

class ValidationError(Exception):
    """验证错误基类"""
    pass

class InvalidEmailError(ValidationError):
    """邮箱格式错误"""
    def __init__(self, email, message="无效的邮箱地址"):
        self.email = email
        self.message = message
        super().__init__(f"{message}: {email}")

def validate_email(email):
    if "@" not in email:
        raise InvalidEmailError(email)
    return True

try:
    validate_email("invalid-email")
except InvalidEmailError as e:
    print(f"错误: {e}")
    print(f"邮箱: {e.email}")

4.1 warnings 模块

用于发出非致命警告, API 弃用通知:

import warnings

# 发出警告
def deprecated_function():
    warnings.warn(
        "deprecated_function 已弃用, 请使用 new_function",
        DeprecationWarning,
        stacklevel=2  # 指向调用者
    )

# 警告类型
warnings.warn("一般警告", UserWarning)
warnings.warn("弃用警告", DeprecationWarning)
warnings.warn("即将移除", PendingDeprecationWarning)
warnings.warn("运行时警告", RuntimeWarning)

# 控制警告行为
warnings.filterwarnings("ignore", category=DeprecationWarning)  # 忽略
warnings.filterwarnings("error", category=UserWarning)  # 转为异常
warnings.filterwarnings("always")  # 始终显示

# 临时修改警告行为
with warnings.catch_warnings():
    warnings.simplefilter("ignore")
    deprecated_function()  # 警告被忽略

命令行控制:

# 忽略所有弃用警告
python -W ignore::DeprecationWarning script.py

# 将警告转为错误
python -W error script.py

5. 上下文管理器

5.1 with 语句

# 传统方式
f = open("file.txt")
try:
    content = f.read()
finally:
    f.close()

# 使用 with
with open("file.txt") as f:
    content = f.read()
# 自动关闭文件

5.2 上下文管理协议

上下文管理器需要实现 __enter____exit__ 方法:

class FileManager:
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        self.file = None
    
    def __enter__(self):
        print("Opening file")
        self.file = open(self.filename, self.mode)
        return self.file
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Closing file")
        if self.file:
            self.file.close()
        # 返回 True 则抑制异常
        return False

with FileManager("test.txt", "w") as f:
    f.write("Hello, World!")

5.3 __exit__ 参数

class SuppressError:
    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is ValueError:
            print(f"抑制 ValueError: {exc_val}")
            return True  # 抑制异常
        return False     # 传播异常

with SuppressError():
    raise ValueError("测试错误")
    print("这行不会执行")

print("继续执行")  # 会执行

5.4 多个上下文管理器

with open("input.txt") as infile, open("output.txt", "w") as outfile:
    content = infile.read()
    outfile.write(content.upper())

6. contextlib 模块

6.1 @contextmanager 装饰器

使用生成器简化上下文管理器:

from contextlib import contextmanager

@contextmanager
def timer():
    import time
    start = time.time()
    yield  # 执行 with 块
    end = time.time()
    print(f"耗时: {end - start:.4f}s")

with timer():
    import time
    time.sleep(1)
# 耗时: 1.0012s

带返回值:

@contextmanager
def open_file(filename, mode):
    f = open(filename, mode)
    try:
        yield f
    finally:
        f.close()

with open_file("test.txt", "w") as f:
    f.write("Hello")

6.2 suppress 抑制异常

from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove("nonexistent.txt")
# 不会抛出异常

# 等价于
try:
    os.remove("nonexistent.txt")
except FileNotFoundError:
    pass

6.3 redirect_stdout

from contextlib import redirect_stdout
from io import StringIO

buffer = StringIO()
with redirect_stdout(buffer):
    print("Hello, World!")

output = buffer.getvalue()
print(f"Captured: {output}")  # Captured: Hello, World!

6.4 ExitStack

动态管理多个上下文管理器:

from contextlib import ExitStack

filenames = ["file1.txt", "file2.txt", "file3.txt"]

with ExitStack() as stack:
    files = [stack.enter_context(open(fn)) for fn in filenames]
    # 所有文件都会在退出时关闭

7. 异常处理模式

7.1 EAFP vs LBYL

LBYL (Look Before You Leap): 先检查再操作

# LBYL
if "key" in d:
    value = d["key"]
else:
    value = default

EAFP (Easier to Ask Forgiveness than Permission): 先操作再处理异常

# EAFP (Python 推荐)
try:
    value = d["key"]
except KeyError:
    value = default

7.2 日志记录

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

try:
    result = risky_operation()
except Exception as e:
    logger.exception("操作失败")  # 包含堆栈信息
    raise

7.3 清理资源

class Connection:
    def __init__(self):
        self.connected = True
        print("Connected")
    
    def close(self):
        self.connected = False
        print("Disconnected")
    
    def __enter__(self):
        return self
    
    def __exit__(self, *args):
        self.close()

# 确保连接被关闭
with Connection() as conn:
    # 使用连接
    pass

8. 调试技巧

8.1 traceback 模块

import traceback

try:
    1 / 0
except ZeroDivisionError:
    # 获取格式化的堆栈信息
    tb = traceback.format_exc()
    print(tb)

8.2 pdb 调试器

import pdb

def buggy_function():
    x = 10
    pdb.set_trace()  # 断点
    y = 0
    return x / y

buggy_function()

pdb 常用命令:

  • n (next): 下一行
  • s (step): 进入函数
  • c (continue): 继续执行
  • p <expr>: 打印表达式
  • q (quit): 退出

8.3 breakpoint() (Python 3.7+)

def buggy_function():
    x = 10
    breakpoint()  # 自动调用 pdb.set_trace()
    y = 0
    return x / y

9. 练习

9.1 安全除法函数

实现一个安全的除法函数, 处理除零和类型错误.

9.2 数据库连接管理器

实现一个数据库连接的上下文管理器, 自动提交或回滚事务.

9.3 重试装饰器

实现一个装饰器, 当函数抛出异常时自动重试 N 次.


10. 思考题

  1. 为什么不建议捕获所有异常 (except:)?
  2. finally 一定会执行吗?
  3. 什么时候应该抑制异常?
  4. EAFP 和 LBYL 各自的适用场景?
  5. 为什么 with 语句比 try/finally 更好?

11. 本周小结

  • 异常处理: try/except/else/finally.
  • 异常层次: Exception 继承树.
  • 自定义异常: 继承 Exception.
  • 上下文管理器: __enter__, __exit__, with 语句.
  • contextlib: @contextmanager, suppress, ExitStack.
  • 处理模式: EAFP vs LBYL.
  • 调试: traceback, pdb, breakpoint.

好的错误处理是健壮程序的基础. 上下文管理器让资源管理变得优雅而安全.

On this page