每天写文件读文件,用 open 函数打开,用完还得 close。少写一次 close ,文件句柄就漏了。漏多了,程序就报错,报错还不好查。
有人用 try finally 包着,代码写了一大堆,看着就累。还有人干脆不关,等垃圾回收,那是赌运气,运气不好线上就崩。
上下文管理器就是干这个的。with open('file.txt', 'r') as f: 这一行,文件打开,代码块跑完,文件自己关。不用你操心。
with 语句的本质,是让对象管理自己的生命周期。你只管用,它管收尾。这跟现实里的租赁一样,签个合同,到期自动退房,不用你半夜爬起来锁门。
不光文件能用。数据库连接,网络 socket ,线程锁,都适用。场景不同,道理一个样。
写一个自己的上下文管理器,也不难。类里实现两个方法,__enter__ 负责准备资源,__exit__ 负责清理资源。比如一个数据库操作,enter 里建立连接,exit 里提交或回滚,再断开连接。用 with 包起来,事务边界清清楚楚。
class Database: def __enter__(self):
self.conn = create_connection()
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type:
self.conn.rollback()
else:
self.conn.commit()
self.conn.close()
代码里的 exc_type 那几个参数,就是用来处理异常的。代码块里抛了异常,exit 会收到异常类型和值。你可以做清理,也可以拦下来不往外抛,全看业务怎么定。
还有一个更省事的写法,用 @contextmanager 装饰器。把 yield 放在中间,yield 之前是 enter 的逻辑,yield 之后是 exit 的逻辑。代码更短,读起来更直白。
from contextlib import contextmanager @contextmanager
def temp_file(name):
f = open(name, 'w')
try:
yield f
finally:
f.close()
用的时候,with temp_file('test.txt') as f: 一行调用。里面写错了,最后一行 close 还是会执行。这个 try finally 是装饰器帮你补上的,你只要写业务,不必写收尾。
很多人不习惯用 with ,是觉得多一层缩进,麻烦。但为了不缩进,付出的代价是忘记关闭资源的隐患。线上跑着跑着,报 “Too many open files” ,查半天,最后发现是某处文件没关。那种坑,踩一次就够。
还有一种场景,就是临时改环境变量,或者临时改当前目录。用上下文管理器,进出都自动还原,不会影响其他代码。
@contextmanager def set_env(key, value):
old = os.environ.get(key)
os.environ[key] = value
try:
yield
finally:
if old is None:
del os.environ[key]
else:
os.environ[key] = old
这个模式在写测试的时候特别有用。测试要临时改某个配置,跑完用例,配置要还原。手写 try finally 很容易漏,用 with 包一层,永远不会漏。
上下文管理器的核心价值,就是把一对动作绑定在一起,保证一前一后必然执行。不用你记,不会忘,也不怕异常打断流程。
下次写代码,凡是遇到“打开什么,用完必须关”的场景,先想想能不能用 with 。文件、网络、锁、临时环境,都用得上。用多了,你会觉得以前手动 close 的日子,纯粹是给自己找麻烦。