Python上下文管理器:你真正需要的三种场景

Python上下文管理器:你真正需要的三种场景。掌握with语句、contextlib以及自定义上下文管理器,实现更好的资源管理。

  • Go
  • MIT
  • 更新于 2026-05-15

大多数关于Python上下文管理器的入门介绍都只给出一个例子——with open("file.txt") as f:——然后就到此为止了。这足以让你学会使用它们,但并没有告诉你什么时候应该自己写一个。

写了几年Python服务之后,我发现自己反复在三种特定场景下用到上下文管理器。每一种都解决了一个理论上try/finally也能解决、但实践中很容易出错的问题。

Python上下文管理器:你真正需要的三种场景 — dibi8.com

场景1:配对"获取"与"释放" #

最经典的场景。你手上有某个必须被释放的东西——一把锁、一个数据库连接、一个临时文件、一个网络套接字——你想确保无论中间的代码是否抛出异常,释放操作都一定会发生。

from contextlib import contextmanager
import threading

_lock = threading.Lock()

@contextmanager
def critical_section():
    _lock.acquire()
    try:
        yield
    finally:
        _lock.release()

with critical_section():
    do_dangerous_thing()

为什么不干脆用try/finally?你当然可以——从调用点的角度看,上下文管理器展开之后本质上就是这样。真正的好处在于,try/finally被放进了辅助函数里,而不是散落在调用点。每个调用方都能免费获得这个保障,也没有人会忘记写finally块。

当我在一个代码库里看到五份重复的try: thing.acquire(); ...; finally: thing.release()时,我就知道有一个上下文管理器正等着被提取出来。

场景2:临时修改"类全局"状态 #

这一点讨论得比较少,但恰恰是上下文管理器真正大显身手的地方。你想在某个代码块执行期间临时翻转某个设置,并且不管这个代码块以什么方式退出,都要把它恢复原状。

import os
from contextlib import contextmanager

@contextmanager
def env(**overrides):
    """Temporarily set environment variables, restoring previous values on exit."""
    saved = {k: os.environ.get(k) for k in overrides}
    os.environ.update({k: str(v) for k, v in overrides.items()})
    try:
        yield
    finally:
        for k, prev in saved.items():
            if prev is None:
                os.environ.pop(k, None)
            else:
                os.environ[k] = prev

with env(DEBUG="1", REGION="us-east-1"):
    run_test_suite()
# Environment is back to whatever it was here.

同样的模式也适用于sys.pathlogging的日志级别、decimal的上下文、被mock掉的属性——凡是符合"保存、修改、恢复"这种结构的场景都适用。测试代码尤其能从中受益;否则的话,替代方案就是那种一旦测试中途抛出异常就会发生泄漏的fixture。

其中比较微妙的一点是要正确地恢复None。一个常见的bug是不做检查直接写os.environ[k] = saved[k]——如果这个变量之前根本不存在,这样写会把字面字符串"None"写进环境变量里。始终应该用pop来恢复"原本不存在"这种状态,而不是写成字符串。

场景3:抑制那些你确实想忽略的异常 #

有时候你确实就是想吞掉某个特定的异常类,然后继续往下执行。Python为此提供了contextlib.suppress

from contextlib import suppress

with suppress(FileNotFoundError):
    os.unlink("maybe-stale.lock")

这比等价的try/except: pass写法要清晰得多,因为它有限的作用范围迫使你必须明确指定异常类型。你不会不小心把所有异常都吞掉——你必须写明具体的异常类。你也不会不小心把清理逻辑之外的代码也一并吞掉;with块的作用范围就是你写下的那部分,不多不少。

我发现这在析构函数和atexit处理器里的清理逻辑中特别有用,因为在那些地方,你真的承受不起清理逻辑本身再抛出异常的后果。

什么时候不该写一个 #

上下文管理器不是没有代价的。每一层with都会引入少量的额外机制,层层嵌套很快就会影响可读性。以下情况我会避免使用:

  • “获取"这一半其实并不需要配对的"释放”——直接调用函数就行。
  • 清理只是尽力而为,而且范围足够小,一段内联的try/finally读起来反而更清楚。
  • 被管理的对象已经由别的东西管理着了(比如说,不要去包装一个框架自带的、本身就已经做了上下文管理的Session对象)。

我用来判断的标准是:“如果我省略了这段清理逻辑,下一个人会不会在不知不觉中泄漏资源?” 如果答案是会,那就写一个上下文管理器。如果不会,一个普通函数就足够了。

关于异步的一点补充 #

async代码中,使用@asynccontextmanagerasync with。结构完全一样;唯一需要记住的是你可以在函数体内await,这让这个模式在"从连接池获取一个连接、执行一次查询、再把它归还"这类场景中显得格外好用。

from contextlib import asynccontextmanager

@asynccontextmanager
async def borrowed(pool):
    conn = await pool.acquire()
    try:
        yield conn
    finally:
        await pool.release(conn)

就是这样。这三种模式覆盖了我写过的上下文管理器里差不多90%的情形。剩下那10%都比较奇特,见到了你自然会认出来。

相关文章 #


推荐工具 #

对于正在构建或部署开源AI工具的开发者,我们推荐:

  • DigitalOcean — 新用户可获得200美元免费额度,14+个全球区域,一键式GPU/CPU云主机,非常适合AI工作负载。
  • Shiyunapi Claude API — Anthropic Claude / OpenAI / DeepSeek API代理。上面大多数AI工具(聊天机器人、代码生成、翻译、搜索等)都需要LLM API密钥——这个代理以约30%的官方价格提供对顶级模型的稳定访问。

联盟链接——支持dibi8.com,对你没有任何额外费用。

参考资料与来源 #

💬 留言讨论