3、上下文管理器:with语句原理、自动资源管理、自定义上下文管理器(__enter__/__exit__)

3.1 为什么需要上下文管理器?

先问大家一个问题:你写文件时,有没有忘记关闭文件句柄的经历?

我承认,我刚入行那会儿经常犯这个错。打开文件,读写完,然后……就没有然后了。文件句柄就这么悬在那里,直到程序结束才被系统回收。这在写小脚本时问题不大,但一旦上了生产环境,文件描述符泄漏可是会搞崩服务器的。

说白了,我们需要一种机制——进入时自动准备资源,退出时自动清理资源。这就是上下文管理器要做的事。

核心思想:把「打开-使用-关闭」这个三板斧封装成一个整体,让你只关心中间的使用环节。

3.2 with语句的基本用法

Python 里最常用的上下文管理器就是文件操作。你看这个写法:

# 传统写法(容易忘关文件)
f = open('data.txt', 'r')
content = f.read()
f.close()  # 这行很容易漏掉

# with语句写法(自动关闭)
with open('data.txt', 'r') as f:
    content = f.read()
# 出了with块,文件自动关闭

我个人习惯用第二种。原因很简单——少写一行代码,少一个bug。你想想看,如果中间发生了异常,传统写法里的 f.close() 根本不会执行到,文件句柄就泄漏了。而 with 语句呢?就算抛出异常,它也会帮你把文件关掉。

小技巧:with 语句可以同时管理多个资源,用逗号分隔即可。

with open('a.txt', 'r') as f1, open('b.txt', 'w') as f2:
    f2.write(f1.read())

3.3 with语句的原理:__enter__ 和 __exit__

with 语句到底是怎么工作的?其实原理很简单,就两个魔法方法:

  • __enter__(self):进入 with 块时调用。返回值会赋给 as 后面的变量。
  • __exit__(self, exc_type, exc_val, exc_tb):离开 with 块时调用。负责清理资源。

执行流程是这样的:

  1. 调用对象的 __enter__ 方法
  2. 把返回值赋给 as 后面的变量
  3. 执行 with 块内的代码
  4. 无论正常结束还是异常,都调用 __exit__ 方法
  5. 如果 __exit__ 返回 True,异常会被吞掉;返回 False(默认),异常继续传播

注意:__exit__ 方法接收三个参数,分别对应异常类型、异常值和 traceback。如果没有异常,这三个参数都是 None。

3.4 自定义上下文管理器

我在项目中遇到过这样一个场景:需要临时修改数据库的连接超时时间,操作完再恢复原值。手动写 try/finally 太啰嗦了,于是我写了个自定义上下文管理器。

来看一个完整的例子:

class ManagedFile:
    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  # 这个值会赋给 as 后面的变量

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.file:
            self.file.close()
        print(f"关闭文件:{self.filename}")

        # 如果发生了异常,打印出来但不阻止程序继续运行
        if exc_type is not None:
            print(f"捕获到异常:{exc_val}")
            return True  # 返回 True 表示异常已处理

# 使用
with ManagedFile('test.txt', 'w') as f:
    f.write('Hello, World!')
    # 如果这里抛出异常,__exit__ 会处理

嗯,这里要注意 __exit__ 的返回值。我曾经踩过一个坑:在上下文管理器里捕获了异常,但忘了返回 True,结果异常还是往外抛了。调试了半天才发现是返回值的问题。

3.5 使用 contextlib 简化实现

如果你觉得写一个类太麻烦,Python 标准库提供了更简洁的方式——contextlib 模块。

我最常用的是 @contextmanager 装饰器:

from contextlib import contextmanager

@contextmanager
def managed_file(filename, mode):
    print(f"打开文件:{filename}")
    f = open(filename, mode)
    try:
        yield f  # yield 之前的代码相当于 __enter__,之后的相当于 __exit__
    finally:
        f.close()
        print(f"关闭文件:{filename}")

# 使用方式完全一样
with managed_file('test.txt', 'w') as f:
    f.write('Hello, World!')

你看,用 yield 把函数分成两段:yield 之前是进入时的操作,yield 之后是退出时的清理。这比写一个类要轻量得多。

实用技巧:contextlib 还提供了 closing() 函数,可以直接把实现了 close() 方法的对象变成上下文管理器。

from contextlib import closing
import urllib.request

with closing(urllib.request.urlopen('http://example.com')) as page:
    for line in page:
        print(line)

3.6 避坑指南与最佳实践

我总结了几条实战经验,分享给大家:

  • 不要滥用上下文管理器:只有真正需要「进入-退出」这种生命周期管理的场景才用。比如数据库连接、文件句柄、锁、网络连接等。
  • __exit__ 里不要轻易返回 True:除非你明确知道要吞掉异常。否则默认返回 False,让异常自然传播。
  • 注意资源释放的顺序:多个上下文管理器嵌套时,退出顺序和进入顺序相反(后进先出)。
  • 用 contextlib 替代手写类:对于简单的场景,@contextmanager 装饰器更简洁,不容易出错。

我曾经踩过的坑:在 __exit__ 里又抛出了异常。这会导致原始异常被覆盖,调试时根本找不到真正的问题。记住:__exit__ 里尽量只做清理工作,不要做复杂的业务逻辑。

3.7 知识体系图

下面这张图帮你理清上下文管理器的核心脉络:

上下文管理器知识体系 上下文管理器 基于类:__enter__/__exit__ 基于装饰器:@contextmanager __enter__() → 返回值 __exit__() → 清理 yield 分割前后 文件操作 数据库连接 线程锁/网络连接 核心:自动资源管理,异常安全

3.8 总结

上下文管理器是 Python 里一个非常优雅的设计。它把「获取资源-使用资源-释放资源」这个固定模式封装起来,让你不用再写一堆 try/finally 的样板代码。

我个人觉得,掌握好上下文管理器,是区分 Python 新手和老手的一个标志。新手可能只会用 with open,而老手会自己写上下文管理器来封装各种资源操作。

记住三个要点:

  • 用 with 语句替代手动 close
  • 用 __enter__/__exit__ 或 @contextmanager 实现自定义管理器
  • __exit__ 里只做清理,不要抛异常

好了,这一章就到这里。代码写多了,你会发现上下文管理器真的是 Python 里最贴心的设计之一。

可可精品资料库