目录

Python 上下文管理器

学习目标

  • 理解上下文管理器的概念和用途
  • 掌握 with 语句的使用
  • 学会使用 contextlib 创建上下文管理器
  • 理解 __enter____exit__ 方法

1. with 语句基础

with 语句用于包裹代码块,确保资源的正确获取和释放。

1.1 文件操作

# 传统方式(容易忘记关闭)
f = open('test.txt', 'w')
f.write('hello')
f.close()

# 使用 with(自动关闭)
with open('test.txt', 'w') as f:
    f.write('hello')
# 文件自动关闭,即使发生异常

1.2 同时管理多个资源

# 读取一个文件,写入另一个文件
with open('input.txt', 'r') as fin, open('output.txt', 'w') as fout:
    content = fin.read()
    fout.write(content.upper())

2. 自定义上下文管理器

2.1 使用类实现

class FileManager:
    """文件管理上下文管理器"""
    
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        self.file = None
    
    def __enter__(self):
        """进入上下文时调用"""
        print(f"打开文件: {self.filename}")
        self.file = open(self.filename, self.mode)
        return self.file
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """退出上下文时调用"""
        print(f"关闭文件: {self.filename}")
        if self.file:
            self.file.close()
        
        # 返回 True 会吞掉异常,返回 False 会抛出异常
        return False

# 使用
with FileManager('test.txt', 'w') as f:
    f.write('测试内容')

输出:

打开文件: test.txt
关闭文件: test.txt

2.2 exit 参数详解

class ExceptionHandler:
    """演示异常处理"""
    
    def __enter__(self):
        print("进入上下文")
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """
        exc_type: 异常类型
        exc_val: 异常值
        exc_tb: 异常追踪信息
        """
        if exc_type:
            print(f"捕获到异常: {exc_type.__name__}: {exc_val}")
            # return True  # 吞掉异常
            return False   # 继续抛出异常
        print("正常退出")
        return True

# 测试正常情况
print("=== 正常情况 ===")
with ExceptionHandler():
    print("执行业务逻辑")

# 测试异常情况
print("\n=== 异常情况 ===")
try:
    with ExceptionHandler():
        print("即将抛出异常")
        raise ValueError("出错了!")
except ValueError:
    print("外部捕获到异常")

3. 使用 contextlib 模块

3.1 contextmanager 装饰器

from contextlib import contextmanager

@contextmanager
def managed_file(filename, mode):
    """使用装饰器创建上下文管理器"""
    print(f"打开: {filename}")
    file = open(filename, mode)
    try:
        yield file  # yield 之前的代码是 __enter__
    finally:
        print(f"关闭: {filename}")
        file.close()
        # yield 之后的代码是 __exit__

# 使用
with managed_file('test.txt', 'w') as f:
    f.write('使用 contextmanager')

3.2 更简洁的示例

from contextlib import contextmanager

@contextmanager
def timer():
    """计时上下文管理器"""
    import time
    start = time.time()
    yield
    elapsed = time.time() - start
    print(f"耗时: {elapsed:.4f} 秒")

# 使用
with timer():
    total = sum(range(1000000))
    print(f"计算结果: {total}")

3.3 数据库事务示例

from contextlib import contextmanager

class MockDB:
    """模拟数据库"""
    def __init__(self):
        self.data = {}
        self.in_transaction = False
    
    def begin(self):
        self.in_transaction = True
        print("开始事务")
    
    def commit(self):
        self.in_transaction = False
        print("提交事务")
    
    def rollback(self):
        self.in_transaction = False
        print("回滚事务")
    
    def insert(self, key, value):
        if not self.in_transaction:
            raise RuntimeError("不在事务中")
        self.data[key] = value
        print(f"插入: {key} = {value}")

db = MockDB()

@contextmanager
def transaction(database):
    """事务上下文管理器"""
    database.begin()
    try:
        yield database
        database.commit()
    except Exception as e:
        database.rollback()
        raise e

# 使用事务
with transaction(db) as conn:
    conn.insert("user1", "Alice")
    conn.insert("user2", "Bob")

4. 实用工具

4.1 suppress - 忽略指定异常

from contextlib import suppress

# 传统方式
try:
    os.remove('不存在的文件.txt')
except FileNotFoundError:
    pass

# 使用 suppress
with suppress(FileNotFoundError):
    import os
    os.remove('不存在的文件.txt')

4.2 redirect_stdout - 重定向输出

from contextlib import redirect_stdout
import io

# 捕获 print 输出
output = io.StringIO()
with redirect_stdout(output):
    print("这条信息被捕获了")
    print("这也是")

result = output.getvalue()
print(f"捕获的内容:\n{result}")

4.3 ExitStack - 管理多个上下文

from contextlib import ExitStack

# 需要管理多个不确定数量的资源
files = ['file1.txt', 'file2.txt', 'file3.txt']

with ExitStack() as stack:
    opened_files = [
        stack.enter_context(open(f, 'w'))
        for f in files
    ]
    
    for i, f in enumerate(opened_files):
        f.write(f"内容 {i + 1}")
    
    # 所有文件会自动关闭

5. 实际应用场景

5.1 临时修改环境

import os
from contextlib import contextmanager

@contextmanager
def temporary_env(**kwargs):
    """临时修改环境变量"""
    old_values = {}
    for key, value in kwargs.items():
        old_values[key] = os.environ.get(key)
        os.environ[key] = value
    
    try:
        yield
    finally:
        for key, old_value in old_values.items():
            if old_value is None:
                os.environ.pop(key, None)
            else:
                os.environ[key] = old_value

# 使用
print(f"当前 PATH: {os.environ.get('MY_VAR', '未设置')}")

with temporary_env(MY_VAR="临时值"):
    print(f"临时 PATH: {os.environ.get('MY_VAR')}")

print(f"恢复后 PATH: {os.environ.get('MY_VAR', '未设置')}")

5.2 临时修改工作目录

import os
from contextlib import contextmanager

@contextmanager
def change_directory(path):
    """临时切换工作目录"""
    original = os.getcwd()
    os.chdir(path)
    try:
        yield original
    finally:
        os.chdir(original)

# 使用
print(f"当前目录: {os.getcwd()}")
# with change_directory('/tmp'):
#     print(f"临时目录: {os.getcwd()}")
# print(f"恢复目录: {os.getcwd()}")

5.3 锁管理

from threading import Lock
from contextlib import contextmanager

lock = Lock()

@contextmanager
def acquire_lock(lock_obj):
    """自动获取和释放锁"""
    lock_obj.acquire()
    try:
        yield
    finally:
        lock_obj.release()

# 使用
with acquire_lock(lock):
    print("持有锁,执行临界区代码")

6. async 上下文管理器

from contextlib import asynccontextmanager

@asynccontextmanager
async def async_resource():
    """异步上下文管理器"""
    print("获取异步资源")
    await asyncio.sleep(0.1)
    try:
        yield "资源"
    finally:
        print("释放异步资源")

# 使用
import asyncio

async def main():
    async with async_resource() as res:
        print(f"使用 {res}")

# asyncio.run(main())

本节小结

  • 上下文管理器:确保资源的正确获取和释放
  • with 语句:包裹代码块,自动调用 __enter____exit__
  • 类实现:实现 __enter____exit__ 方法
  • contextmanager:使用装饰器更简洁地创建上下文管理器
  • 实用工具suppressredirect_stdoutExitStack
  • 应用场景:文件操作、事务管理、锁、临时环境修改

练习

  1. 创建一个上下文管理器,自动记录代码块的执行时间
  2. 实现一个上下文管理器,临时修改日志级别
  3. 使用 ExitStack 同时打开多个文件并合并内容
  4. 创建一个上下文管理器,实现简单的性能分析(统计函数调用次数和时间)