返回专题首页

Python 专题

用 with 管好资源:上下文管理器与 contextlib 实战

很多资源都有“申请”和“释放”两个动作,文件句柄如此,数据库连接如此,锁和事务也是如此。如果释放动作完全靠人记忆,代码就会变得又脆弱又难维护。

Python 专题第 15 篇 / 39 篇4 分钟

很多资源都有“申请”和“释放”两个动作,文件句柄如此,数据库连接如此,锁和事务也是如此。如果释放动作完全靠人记忆,代码就会变得又脆弱又难维护。

这一节我们会围绕 with 语句展开,理解上下文管理器为什么能优雅地处理资源生命周期,以及如何用 contextlib 写出更轻巧、更符合 Python 风格的资源管理代码。

为什么要使用 with

资源管理最麻烦的地方,从来不是“怎么拿到资源”,而是“各种异常路径下怎么保证它被正确释放”。

例如文件操作:

file = open("demo.txt", "r", encoding="utf-8")
content = file.read()
file.close()

如果中间出错,close() 可能就不会被执行。于是资源泄漏、文件句柄占用、锁没有释放等问题就来了。

with 的价值就在于:把“进入资源使用阶段”和“退出资源使用阶段”这两个动作绑定起来。

写成:

with open("demo.txt", "r", encoding="utf-8") as file:
    content = file.read()

不管中间是否出错,退出 with 代码块时,清理逻辑都会按协议执行。

文件对象中的上下文管理

文件对象是最经典的上下文管理器例子,也是最适合初学者建立心智的入口。

当你写下:

with open("demo.txt", "w", encoding="utf-8") as file:
    file.write("hello")

你可以把它理解成:

  • 进入块之前,文件被打开;
  • 块内可以安全使用这个文件对象;
  • 块结束时,无论是否抛异常,文件都会被正确关闭。

这其实就是资源生命周期管理的缩影。后面你在数据库连接、线程锁、事务、临时目录这些场景里看到的 with,本质上都是同一个思路。

自定义上下文管理器

如果你自己的对象也想支持 with,就需要实现上下文管理协议。

__enter____exit__

最基础的方式,是实现 __enter____exit__

class DemoResource:
    def __enter__(self):
        print("open")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("close")

使用时:

with DemoResource() as resource:
    print("using resource")

其中:

  • __enter__ 负责进入上下文时的准备动作,并决定 as 后面拿到什么对象;
  • __exit__ 负责离开上下文时的清理动作,还可以根据异常信息决定是否吞掉异常。

不过工程里通常不建议轻易吞掉异常,除非你非常明确地知道这么做合理。大多数时候,清理完资源后继续让异常上抛,会更符合调试和定位问题的需要。

contextlib 简化实现

虽然手写类没问题,但有些场景只是想做一个轻量上下文管理器,用类会显得稍微重了一点。

这时 contextlib 就很合适。

@contextmanager

contextlib.contextmanager 允许你用生成器风格来定义上下文管理器:

from contextlib import contextmanager


@contextmanager
def managed_resource():
    print("open")
    try:
        yield "resource"
    finally:
        print("close")

使用方式依旧是:

with managed_resource() as resource:
    print(resource)

这种写法特别适合那些逻辑非常线性的场景:进入前准备,yield 把资源交出去,退出时统一清理。

所以 @contextmanager 的价值,并不是“更高级”,而是让一些一次性、轻量级资源管理代码更紧凑。

典型应用:连接、锁与事务

上下文管理器之所以重要,是因为它覆盖的场景远不止文件。

比如数据库连接:

  • 进入时拿连接;
  • 块内执行 SQL;
  • 退出时关闭连接或归还连接池。

再比如线程锁:

  • 进入时加锁;
  • 块内执行临界区逻辑;
  • 退出时自动释放锁。

事务处理也是同样思路:

  • 进入时开启事务;
  • 块内执行若干数据库操作;
  • 没异常就提交,有异常就回滚。

只要你建立了“资源有生命周期”的心智,就会发现 with 并不是一个只用于文件读写的小语法,而是 Python 用来表达资源边界的核心工具。

总结与预告

这一节我们围绕 with 建立了资源管理的基本心智,理解了上下文管理器为什么能把申请和释放动作组织得更稳定。比起记住几个方法名,更重要的是开始形成“资源有生命周期”的意识。

下一节我们会继续处理结构化数据表达的问题,看看 dataclassNamedTupleTypedDict 如何帮我们把数据描述得更清楚。