大家好,我是沉默小皮,让大家更容易的学习Python3,今天我们来看下:with 关键字。这个东西你用没写过,但一定见过。
一、先聊聊没有 with 的日子有多痛苦
写程序经常要跟各种资源打交道:打开文件、连接数据库、获取线程锁……这些资源有个共同特点:用完了必须手动释放。忘记关了会怎样?文件句柄泄露、数据库连接池爆满、程序越来越卡,最后崩溃。
看看传统写法有多啰嗦:
# 没有 with 的年代,大家是这么写的
file = open('data.txt', 'r')
try:
content = file.read()
# 处理文件内容...
finally:
file.close() # 不管发不发生异常,都要记得关
这段代码有几个问题:
- 异常处理复杂:如果
open 本身就失败了,close 还会再报错
你可能觉得“不就多写几行嘛”,但如果一个程序里要打开十几个文件、连好几个数据库,这种重复代码能把人逼疯。
这就是 with 要解决的问题。
二、with 的基本用法:简单到令人发指
用 with 重写上面的例子:
with open('data.txt', 'r') as file:
content = file.read()
print(content)
# 出了 with 块,文件自动关闭,不用你管
是不是清爽多了?with 后面跟一个“上下文管理器”(比如 open() 返回的文件对象),as 给这个对象取个名字,然后在缩进的代码块里用它。代码块执行完——不管是正常结束还是中间抛了异常——Python 都会自动调用对象的“清理方法”(对于文件来说就是 close())。
这里我踩过坑:刚开始学的时候,我以为 with 只对文件有用。后来才知道,只要是支持“上下文管理协议”的对象都能用。啥是上下文管理协议?简单说就是实现了 __enter__ 和 __exit__ 两个方法的对象。很多 Python 内置对象都支持,比如数据库连接、线程锁、decimal 上下文等等。
三、with 是怎么工作的?(简单了解即可)
如果好奇背后的原理,with 大致做了这么几件事:
- 调用
open('data.txt', 'r') 得到一个文件对象 - 调用这个对象的
__enter__() 方法,返回值赋给 as 后面的 file - 不管块里的代码是否抛异常,都会调用
file.__exit__() 来做清理
__exit__() 方法还可以决定要不要“吞掉”异常(返回 True 表示异常已处理,不再往外抛),但日常开发基本用不到这个特性。
四、多个资源一起管理:一条 with 搞定
有时候你需要同时打开两个文件,比如从一个文件读内容写到另一个文件:
# 传统写法:嵌套 with 或者写两个 try-finally,都很丑
with open('input.txt', 'r') as infile:
with open('output.txt', 'w') as outfile:
content = infile.read()
outfile.write(content)
# Python 支持在一个 with 里写多个,用逗号隔开
with open('input.txt', 'r') as infile, open('output.txt', 'w') as outfile:
content = infile.read()
outfile.write(content.upper())
多资源场景下,这种写法比嵌套 with 清晰很多。而且不管哪个资源出了问题,所有已经成功打开的资源都会被正确关闭——顺序和打开相反,保证不会出错。
五、其他常见应用场景
5.1 数据库连接
import sqlite3
# 传统写法:try-finally 关连接
conn = sqlite3.connect('my.db')
try:
cursor = conn.cursor()
cursor.execute('SELECT * FROM users')
# ...
finally:
conn.close()
# with 写法:自动提交或回滚,自动关闭连接
with sqlite3.connect('my.db') as conn:
cursor = conn.cursor()
cursor.execute('SELECT * FROM users')
results = cursor.fetchall()
# 出了 with 块,连接自动关闭,事务自动提交
注意:sqlite3.connect() 返回的连接对象本身就是上下文管理器,所以可以直接用。
5.2 线程锁
import threading
lock = threading.Lock()
# 传统方式
lock.acquire()
try:
# 临界区代码
print("线程安全的操作")
finally:
lock.release()
# with 方式:自动获取和释放
with lock:
print("线程安全的操作")
5.3 临时修改系统状态(比如精度设置)
import decimal
# 临时设置高精度,用完自动恢复
with decimal.localcontext() as ctx:
ctx.prec = 42# 设置精度为42位
# 在这里进行高精度计算...
result = decimal.Decimal(1) / decimal.Decimal(7)
# 出了这个块,精度恢复成原来的值
这个例子可能有点抽象,但用 with 来管理“临时状态”是非常优雅的模式——你不用担心改完忘了改回来。
六、自己动手写一个上下文管理器
有两种方式:用类实现,或者用 contextlib 模块。
6.1 类实现:需要写 __enter__ 和 __exit__
import time
classTimer:
"""一个简单的计时器上下文管理器"""
def__enter__(self):
self.start = time.time()
return self # 返回值会赋给 as 后面的变量
def__exit__(self, exc_type, exc_val, exc_tb):
self.end = time.time()
print(f"代码块耗时: {self.end - self.start:.2f} 秒")
# 返回 False,表示如果有异常,继续向外抛出
returnFalse
# 使用
with Timer() as t:
# 模拟耗时操作
sum(range(10_000_000))
# 输出类似:代码块耗时: 0.45 秒
这里 __exit__ 接收的三个参数分别是异常类型、异常值、异常追踪信息。如果你在 with 块里处理了异常并想“吞掉”它,就返回 True;否则返回 False(或者不写 return,默认返回 None 等价于 False)。
6.2 用 @contextmanager 装饰器:更简单
from contextlib import contextmanager
@contextmanager
deftimer():
import time
start = time.time()
try:
yield# 这里的代码会在 with 块里执行
finally:
end = time.time()
print(f"耗时: {end - start:.2f} 秒")
# 使用
with timer():
sum(range(10_000_000))
@contextmanager 把一个生成器函数变成了上下文管理器。yield 之前的代码相当于 __enter__,yield 之后的代码(通常放在 finally 里)相当于 __exit__。这种方式写起来更直观,适合快速实现简单的上下文管理器。
几点实在的建议
能用 with 的地方就别手动 try-finally
这是 Python 社区公认的最佳实践。不仅代码更短,还避免忘记释放资源。我见过太多线上事故是因为异常路径下忘记 close 连接导致的。
with 不是只能管文件
所有实现了上下文管理协议的对象都可以用:数据库连接、锁、decimal 上下文、tempfile 临时文件、mock 对象等等。写代码时多留意标准库文档,看看哪些对象支持 with。
自定义上下文管理器时别忘了处理异常
如果你的 __exit__ 方法会做资源清理,记得把清理代码放在 try-finally 或使用 contextlib 来保证清理逻辑一定会执行。而且 __exit__ 里如果发生新的异常,会覆盖掉原来 with 块里的异常,要特别小心。
多个资源用 with A as a, B as b,而不是嵌套
嵌套 with 会让代码缩进越来越深,可读性差。Python 支持在一个 with 语句里管理多个上下文管理器,用逗号隔开就行。但注意:Python 3.10 之前,如果第二个上下文管理器初始化失败,第一个的 __exit__ 可能不会被正确调用。3.10 之后修复了这个 bug。如果你还在用老版本,多个资源还是用嵌套更稳妥。
别在 with 块里写太多无关代码
with 块应该只放和这个资源直接相关的操作。比如打开文件后,在里面做大量计算、调用其他函数,会导致文件一直开着不关,虽然最后也会关,但持有时间过长可能影响其他进程。建议:在 with 块里快速读完文件或写完数据,然后出去再处理。
open 函数本身就是上下文管理器,不需要额外包装
新手容易写 with open(...) as f: 然后在里面自己调用 f.close(),画蛇添足。with 会自动关,你不用再手动关一次。手动关两次不会报错,但显得不专业。