Python教程27:上下文管理器与with语句

“始有终,开必合。”

我们已经多次使用with语句打开文件,今天深入学习它的原理——上下文管理器(Context Manager)。这是Python资源管理的利器,让代码更安全、更优雅。

1. 什么是上下文管理器

问题场景

资源(文件、网络连接、数据库连接、锁)需要正确管理:

 1# 问题代码:可能忘记关闭
 2file = open("data.txt")
 3content = file.read()
 4file.close()  # 忘记关闭?异常时未关闭?
 5
 6# try-finally保证关闭
 7file = open("data.txt")
 8try:
 9    content = file.read()
10finally:
11    file.close()  # 总是执行
12
13# 但每次都这样写很繁琐...

上下文管理器(Context Manager)

  • 自动管理资源的生命周期
  • 确保资源的正确获取和释放
  • 使用with语句
  • 即使发生异常也能正确清理
1# with语句:简洁且安全
2with open("data.txt") as file:
3    content = file.read()
4# 自动关闭文件,即使有异常也会关闭

with语句的优势

  1. 自动清理:离开with块自动释放资源
  2. 异常安全:即使发生异常也会执行清理
  3. 代码简洁:不需要显式的try-finally
  4. 可读性强:明确表达资源管理意图

2. 上下文管理器协议

上下文管理器需要实现两个魔术方法:

enter()

进入with块时调用,返回资源对象:

1def __enter__(self):
2    """
3    进入上下文时调用
4    - 获取资源
5    - 返回的对象赋给as后的变量
6    """
7    return self  # 或其他资源对象

exit()

离开with块时调用,执行清理操作:

 1def __exit__(self, exc_type, exc_value, traceback):
 2    """
 3    退出上下文时调用
 4    
 5    Args:
 6        exc_type: 异常类型(无异常时为None)
 7        exc_value: 异常值
 8        traceback: 异常追踪信息
 9    
10    Returns:
11        True: 抑制异常(异常不会传播)
12        False/None: 异常正常传播
13    """
14    # 执行清理操作
15    self.cleanup()
16    
17    # 返回True会抑制异常
18    return False

3. 创建上下文管理器

方法1:类实现

 1class FileManager:
 2    """
 3    自定义文件管理器
 4    - 展示上下文管理器的基本实现
 5    - __enter__打开文件
 6    - __exit__关闭文件
 7    """
 8    def __init__(self, filename, mode):
 9        self.filename = filename
10        self.mode = mode
11        self.file = None
12    
13    def __enter__(self):
14        """打开文件"""
15        print(f"打开文件:{self.filename}")
16        self.file = open(self.filename, self.mode)
17        return self.file
18    
19    def __exit__(self, exc_type, exc_value, traceback):
20        """关闭文件"""
21        if self.file:
22            print(f"关闭文件:{self.filename}")
23            self.file.close()
24        
25        if exc_type:
26            print(f"发生异常:{exc_type.__name__}: {exc_value}")
27        
28        # 返回False,让异常正常传播
29        return False
30
31# 使用
32with FileManager("test.txt", "w") as f:
33    f.write("Hello, World!")
34    # raise ValueError("测试异常")  # 取消注释测试
35# 输出:
36# 打开文件:test.txt
37# 关闭文件:test.txt

方法2:contextlib.contextmanager装饰器

contextlib是Python标准库,提供上下文管理器工具:

 1from contextlib import contextmanager
 2
 3@contextmanager
 4def file_manager(filename, mode):
 5    """
 6    使用装饰器创建上下文管理器
 7    - yield之前是__enter__
 8    - yield的值是as后的变量
 9    - yield之后是__exit__
10    """
11    print(f"打开文件:{filename}")
12    f = open(filename, mode)
13    
14    try:
15        yield f  # yield的值赋给as变量
16    finally:
17        print(f"关闭文件:{filename}")
18        f.close()
19
20# 使用
21with file_manager("test.txt", "w") as f:
22    f.write("使用装饰器版本")

@contextmanager的工作原理

  1. yield之前:相当于__enter__,获取资源
  2. yield value:value赋给as后的变量
  3. yield之后(finally块):相当于__exit__,清理资源

4. 实用上下文管理器示例

示例1:数据库连接

 1import sqlite3
 2from contextlib import contextmanager
 3
 4@contextmanager
 5def database_connection(db_name):
 6    """
 7    数据库连接上下文管理器
 8    - 自动管理数据库连接的打开和关闭
 9    - 确保连接总是被关闭,避免资源泄漏
10    """
11    conn = sqlite3.connect(db_name)
12    print(f"连接数据库:{db_name}")
13    
14    try:
15        yield conn
16    finally:
17        conn.close()
18        print("关闭数据库连接")
19
20# 使用
21with database_connection("test.db") as conn:
22    cursor = conn.cursor()
23    cursor.execute("SELECT * FROM users")
24# 自动关闭连接

示例2:计时器

 1import time
 2from contextlib import contextmanager
 3
 4@contextmanager
 5def timer(name):
 6    """
 7    代码块计时器
 8    - 测量with块内代码的执行时间
 9    - 常用于性能分析
10    """
11    start = time.time()
12    print(f"开始计时:{name}")
13    
14    yield  # 不需要返回值
15    
16    end = time.time()
17    print(f"{name}执行时间:{end - start:.4f}秒")
18
19# 使用
20with timer("数据处理"):
21    # 模拟耗时操作
22    time.sleep(1)
23    result = sum(range(1000000))
24# 输出:数据处理执行时间:1.0234秒

示例3:临时目录

 1import os
 2import shutil
 3import tempfile
 4from contextlib import contextmanager
 5
 6@contextmanager
 7def temporary_directory():
 8    """
 9    临时目录上下文管理器
10    - 创建临时目录
11    - 使用后自动删除
12    """
13    tmpdir = tempfile.mkdtemp()
14    print(f"创建临时目录:{tmpdir}")
15    
16    try:
17        yield tmpdir
18    finally:
19        shutil.rmtree(tmpdir)
20        print("删除临时目录")
21
22# 使用
23with temporary_directory() as tmpdir:
24    # 在临时目录中工作
25    file_path = os.path.join(tmpdir, "temp.txt")
26    with open(file_path, "w") as f:
27        f.write("临时文件")
28# 临时目录自动删除

示例4:修改工作目录

 1import os
 2from contextlib import contextmanager
 3
 4@contextmanager
 5def change_directory(path):
 6    """
 7    临时切换工作目录
 8    - 进入with时切换到指定目录
 9    - 退出with时恢复原目录
10    """
11    old_dir = os.getcwd()
12    os.chdir(path)
13    print(f"切换到:{path}")
14    
15    try:
16        yield
17    finally:
18        os.chdir(old_dir)
19        print(f"恢复到:{old_dir}")
20
21# 使用
22with change_directory("/tmp"):
23    print(f"当前目录:{os.getcwd()}")
24    # 在/tmp中工作
25print(f"恢复后目录:{os.getcwd()}")

5. contextlib模块

contextlib提供了更多上下文管理器工具:

suppress - 抑制异常

 1from contextlib import suppress
 2
 3# suppress用于临时忽略特定异常
 4# 来自contextlib标准库
 5
 6# 不使用suppress
 7try:
 8    os.remove("nonexistent.txt")
 9except FileNotFoundError:
10    pass  # 忽略文件不存在的错误
11
12# 使用suppress(更清晰)
13with suppress(FileNotFoundError):
14    os.remove("nonexistent.txt")

redirect_stdout/stderr - 重定向输出

 1from contextlib import redirect_stdout
 2import io
 3
 4# 重定向标准输出
 5# 用于捕获print输出
 6output = io.StringIO()
 7with redirect_stdout(output):
 8    print("这段文字被捕获了")
 9    print("不会显示在终端")
10
11captured = output.getvalue()
12print(f"捕获的内容:{captured}")

closing - 确保close()被调用

1from contextlib import closing
2from urllib.request import urlopen
3
4# closing确保对象的close()方法被调用
5# 适用于有close()方法但不是上下文管理器的对象
6with closing(urlopen("http://example.com")) as page:
7    content = page.read()
8# 自动调用page.close()

ExitStack - 动态管理多个上下文

 1from contextlib import ExitStack
 2
 3# ExitStack允许动态地进入/退出多个上下文
 4# 适用于数量不固定的资源管理
 5files = ["file1.txt", "file2.txt", "file3.txt"]
 6
 7with ExitStack() as stack:
 8    # 动态打开多个文件
 9    file_objects = [
10        stack.enter_context(open(fname))
11        for fname in files
12    ]
13    
14    # 使用所有文件
15    for f in file_objects:
16        content = f.read()
17# 所有文件自动关闭

6. 嵌套with语句

多个with

1# Python 2.6之前
2with open("input.txt") as infile:
3    with open("output.txt", "w") as outfile:
4        outfile.write(infile.read())
5
6# Python 2.7+:一行多个with
7with open("input.txt") as infile, open("output.txt", "w") as outfile:
8    outfile.write(infile.read())

7. 异常处理

__exit__的返回值控制异常传播:

 1class ExceptionHandler:
 2    def __enter__(self):
 3        return self
 4    
 5    def __exit__(self, exc_type, exc_value, traceback):
 6        if exc_type is ValueError:
 7            print(f"捕获ValueError:{exc_value}")
 8            return True  # 抑制ValueError
 9        
10        # 其他异常正常传播
11        return False
12
13# 使用
14with ExceptionHandler():
15    raise ValueError("这个异常会被抑制")
16    print("这行不会执行")
17print("继续执行")  # 会执行
18
19with ExceptionHandler():
20    raise TypeError("这个异常会传播")  # TypeError不会被抑制
21print("不会执行")

8. 最佳实践

1. 优先使用with

1# 不好
2f = open("data.txt")
3content = f.read()
4f.close()
5
6# 好
7with open("data.txt") as f:
8    content = f.read()

2. 对所有需要清理的资源使用上下文管理器

 1# 文件
 2with open(file) as f: pass
 3
 4# 锁
 5with lock: pass
 6
 7# 数据库连接
 8with get_connection() as conn: pass
 9
10# 线程池
11with ThreadPoolExecutor() as executor: pass

3. 自定义资源类时实现上下文管理器

 1class Resource:
 2    def __init__(self):
 3        self.resource = None
 4    
 5    def __enter__(self):
 6        self.resource = acquire_resource()
 7        return self.resource
 8    
 9    def __exit__(self, *args):
10        release_resource(self.resource)

9. 小结

今天我们学习了上下文管理器:

  • 定义:自动管理资源生命周期的对象
  • 协议__enter____exit__方法
  • with语句:自动调用协议方法
  • 创建方式
    • 类实现(实现__enter__/exit
    • @contextmanager装饰器(更简洁)
  • contextlib模块:suppress、redirect、closing、ExitStack
  • 应用场景:文件、数据库、锁、计时、临时资源
  • 最佳实践:优先使用with、为资源类实现协议

上下文管理器是Python资源管理的标准做法,让代码更安全、更Pythonic。


练习题

  1. 创建一个上下文管理器,在进入时打印开始时间,退出时打印结束时间
  2. 实现一个上下文管理器来管理数据库事务(commit/rollback)
  3. 使用@contextmanager创建一个临时修改全局变量的上下文管理器

本文代码示例

关注公众号:极客老墨

更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。

极客老墨微信公众号二维码

相关阅读