Python上下文管理器:你真正需要的三种场景
Python上下文管理器:你真正需要的三种场景。掌握with语句、contextlib以及自定义上下文管理器,实现更好的资源管理。
- Go
- MIT
- 更新于 2026-05-15
大多数关于Python上下文管理器的入门介绍都只给出一个例子——with open("file.txt") as f:——然后就到此为止了。这足以让你学会使用它们,但并没有告诉你什么时候应该自己写一个。
写了几年Python服务之后,我发现自己反复在三种特定场景下用到上下文管理器。每一种都解决了一个理论上try/finally也能解决、但实践中很容易出错的问题。

场景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.path、logging的日志级别、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代码中,使用@asynccontextmanager和async 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%都比较奇特,见到了你自然会认出来。
相关文章 #
- Scrapling评测:更快、更隐蔽的Python网页抓取方案 — 进阶Python网页抓取
- 不迷路地读懂Postgres中的EXPLAIN ANALYZE — 数据库性能优化
- 免费Claude Code:搭配任意AI提供商免费使用Claude Code CLI — AI辅助编程
推荐工具 #
对于正在构建或部署开源AI工具的开发者,我们推荐:
- DigitalOcean — 新用户可获得200美元免费额度,14+个全球区域,一键式GPU/CPU云主机,非常适合AI工作负载。
- Shiyunapi Claude API — Anthropic Claude / OpenAI / DeepSeek API代理。上面大多数AI工具(聊天机器人、代码生成、翻译、搜索等)都需要LLM API密钥——这个代理以约30%的官方价格提供对顶级模型的稳定访问。
联盟链接——支持dibi8.com,对你没有任何额外费用。
💬 留言讨论