很多资源都有“申请”和“释放”两个动作,文件句柄如此,数据库连接如此,锁和事务也是如此。如果释放动作完全靠人记忆,代码就会变得又脆弱又难维护。
这一节我们会围绕 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 建立了资源管理的基本心智,理解了上下文管理器为什么能把申请和释放动作组织得更稳定。比起记住几个方法名,更重要的是开始形成“资源有生命周期”的意识。
下一节我们会继续处理结构化数据表达的问题,看看 dataclass、NamedTuple 和 TypedDict 如何帮我们把数据描述得更清楚。