上下文管理器:with语句、自动资源管理、自定义上下文管理器
说实话,刚学Python那会儿,我总觉得with open() as f就是个偷懒的写法。直到有一次线上服务因为文件句柄泄漏挂了,我才真正明白——这玩意儿不是偷懒,是保命用的。
上下文管理器,说白了就是帮你管好资源的。你只管用,它负责开和关。嗯,就像你请了个管家,饭端上来你吃,吃完他收拾,你连碗都不用碰。
with语句:最常用的资源管家
先看最常见的用法:
# 传统写法
f = open('data.txt', 'r')
content = f.read()
f.close()
# with写法
with open('data.txt', 'r') as f:
content = f.read()
两段代码效果一样,但第二段安全得多。为什么?因为with保证文件一定会被关闭,哪怕中间抛了异常。
核心要点:with语句在进入时调用__enter__,在退出时调用__exit__。不管正常退出还是异常退出,__exit__都会被执行。
我个人习惯,凡是涉及资源操作的,一律用with。文件、网络连接、数据库会话、锁……一个都别放过。
自动资源管理:不止是文件
很多人以为上下文管理器只能管文件,其实它能管的东西多了去了。我列几个常见的:
| 资源类型 | 示例 | 说明 |
|---|---|---|
| 文件 | with open(...) as f |
自动关闭文件句柄 |
| 线程锁 | with lock: |
自动获取和释放锁 |
| 网络连接 | with socket.create_connection(...) as s |
自动关闭连接 |
| 数据库游标 | with connection.cursor() as cur |
自动提交/回滚事务 |
| 临时文件 | with tempfile.NamedTemporaryFile() as tf |
自动删除临时文件 |
你想想看,如果每个资源都要手动close(),代码里得塞多少try...finally?而且人总会忘的。我曾经排查过一个bug,查了两天才发现是某个网络连接没关,导致连接池被耗尽。从那以后,我写代码就养成了条件反射——看到资源,先写with。
自定义上下文管理器:自己造管家
Python允许你自定义上下文管理器。有两种方式:
方式一:基于类
class Timer:
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.end = time.time()
print(f'耗时:{self.end - self.start:.3f}秒')
# 返回False表示不抑制异常,True表示抑制
return False
# 使用
with Timer() as t:
time.sleep(1)
# 输出:耗时:1.001秒
这里要注意__exit__的三个参数:exc_type是异常类型,exc_val是异常实例,exc_tb是回溯信息。如果没有异常,这三个都是None。
避坑指南:__exit__返回True会抑制异常。我曾经在项目里不小心返回了True,结果异常被吞掉了,排查了半天才发现。除非你明确知道自己在做什么,否则建议返回False。
方式二:基于生成器(更简洁)
from contextlib import contextmanager
@contextmanager
def timer():
start = time.time()
try:
yield # 这里就是with块执行的地方
finally:
end = time.time()
print(f'耗时:{end - start:.3f}秒')
# 使用
with timer():
time.sleep(1)
我个人更偏爱生成器方式,代码量少,逻辑清晰。但要注意,yield前后的代码分别对应__enter__和__exit__,try...finally保证异常时也能执行清理逻辑。
知识体系总览
下面这张图帮你理清上下文管理器的核心脉络:
进阶技巧:嵌套与组合
多个上下文管理器可以嵌套使用:
# 方式一:嵌套
with open('a.txt') as f1:
with open('b.txt') as f2:
data = f1.read() + f2.read()
# 方式二:合并(Python 3.10+)
with (
open('a.txt') as f1,
open('b.txt') as f2
):
data = f1.read() + f2.read()
我个人推荐方式二,代码更扁平,可读性更好。但要注意,Python 3.9及以下版本不支持这种写法,得用contextlib.ExitStack:
from contextlib import ExitStack
with ExitStack() as stack:
files = [stack.enter_context(open(f'file_{i}.txt')) for i in range(10)]
# 所有文件都会自动关闭
小技巧:ExitStack特别适合动态数量的资源管理。我在写爬虫时经常用它来管理多个网络连接,优雅又安全。
避坑总结
- 别吞异常:
__exit__返回True会吞掉异常,除非你有特殊需求,否则返回False。 - 生成器版别忘了
try...finally:用@contextmanager时,yield前后的代码必须用try...finally包起来,否则异常时清理逻辑不会执行。 - 别滥用:上下文管理器适合资源管理,不适合普通逻辑。我见过有人把
if...else也写成上下文管理器,那纯粹是炫技,不实用。
好了,上下文管理器就聊到这儿。记住一句话:凡是需要成对操作(开/关、锁/解锁、连接/断开)的地方,都值得考虑用上下文管理器。写代码嘛,省心最重要。