3、上下文管理器:with语句原理、自动资源管理、自定义上下文管理器(__enter__/__exit__)
3.1 为什么需要上下文管理器?
先问大家一个问题:你写文件时,有没有忘记关闭文件句柄的经历?
我承认,我刚入行那会儿经常犯这个错。打开文件,读写完,然后……就没有然后了。文件句柄就这么悬在那里,直到程序结束才被系统回收。这在写小脚本时问题不大,但一旦上了生产环境,文件描述符泄漏可是会搞崩服务器的。
说白了,我们需要一种机制——进入时自动准备资源,退出时自动清理资源。这就是上下文管理器要做的事。
核心思想:把「打开-使用-关闭」这个三板斧封装成一个整体,让你只关心中间的使用环节。
3.2 with语句的基本用法
Python 里最常用的上下文管理器就是文件操作。你看这个写法:
# 传统写法(容易忘关文件)
f = open('data.txt', 'r')
content = f.read()
f.close() # 这行很容易漏掉
# with语句写法(自动关闭)
with open('data.txt', 'r') as f:
content = f.read()
# 出了with块,文件自动关闭
我个人习惯用第二种。原因很简单——少写一行代码,少一个bug。你想想看,如果中间发生了异常,传统写法里的 f.close() 根本不会执行到,文件句柄就泄漏了。而 with 语句呢?就算抛出异常,它也会帮你把文件关掉。
小技巧:with 语句可以同时管理多个资源,用逗号分隔即可。
with open('a.txt', 'r') as f1, open('b.txt', 'w') as f2:
f2.write(f1.read())
3.3 with语句的原理:__enter__ 和 __exit__
with 语句到底是怎么工作的?其实原理很简单,就两个魔法方法:
- __enter__(self):进入 with 块时调用。返回值会赋给 as 后面的变量。
- __exit__(self, exc_type, exc_val, exc_tb):离开 with 块时调用。负责清理资源。
执行流程是这样的:
- 调用对象的
__enter__方法 - 把返回值赋给 as 后面的变量
- 执行 with 块内的代码
- 无论正常结束还是异常,都调用
__exit__方法 - 如果
__exit__返回 True,异常会被吞掉;返回 False(默认),异常继续传播
注意:__exit__ 方法接收三个参数,分别对应异常类型、异常值和 traceback。如果没有异常,这三个参数都是 None。
3.4 自定义上下文管理器
我在项目中遇到过这样一个场景:需要临时修改数据库的连接超时时间,操作完再恢复原值。手动写 try/finally 太啰嗦了,于是我写了个自定义上下文管理器。
来看一个完整的例子:
class ManagedFile:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
print(f"打开文件:{self.filename}")
self.file = open(self.filename, self.mode)
return self.file # 这个值会赋给 as 后面的变量
def __exit__(self, exc_type, exc_val, exc_tb):
if self.file:
self.file.close()
print(f"关闭文件:{self.filename}")
# 如果发生了异常,打印出来但不阻止程序继续运行
if exc_type is not None:
print(f"捕获到异常:{exc_val}")
return True # 返回 True 表示异常已处理
# 使用
with ManagedFile('test.txt', 'w') as f:
f.write('Hello, World!')
# 如果这里抛出异常,__exit__ 会处理
嗯,这里要注意 __exit__ 的返回值。我曾经踩过一个坑:在上下文管理器里捕获了异常,但忘了返回 True,结果异常还是往外抛了。调试了半天才发现是返回值的问题。
3.5 使用 contextlib 简化实现
如果你觉得写一个类太麻烦,Python 标准库提供了更简洁的方式——contextlib 模块。
我最常用的是 @contextmanager 装饰器:
from contextlib import contextmanager
@contextmanager
def managed_file(filename, mode):
print(f"打开文件:{filename}")
f = open(filename, mode)
try:
yield f # yield 之前的代码相当于 __enter__,之后的相当于 __exit__
finally:
f.close()
print(f"关闭文件:{filename}")
# 使用方式完全一样
with managed_file('test.txt', 'w') as f:
f.write('Hello, World!')
你看,用 yield 把函数分成两段:yield 之前是进入时的操作,yield 之后是退出时的清理。这比写一个类要轻量得多。
实用技巧:contextlib 还提供了 closing() 函数,可以直接把实现了 close() 方法的对象变成上下文管理器。
from contextlib import closing
import urllib.request
with closing(urllib.request.urlopen('http://example.com')) as page:
for line in page:
print(line)
3.6 避坑指南与最佳实践
我总结了几条实战经验,分享给大家:
- 不要滥用上下文管理器:只有真正需要「进入-退出」这种生命周期管理的场景才用。比如数据库连接、文件句柄、锁、网络连接等。
- __exit__ 里不要轻易返回 True:除非你明确知道要吞掉异常。否则默认返回 False,让异常自然传播。
- 注意资源释放的顺序:多个上下文管理器嵌套时,退出顺序和进入顺序相反(后进先出)。
- 用 contextlib 替代手写类:对于简单的场景,@contextmanager 装饰器更简洁,不容易出错。
我曾经踩过的坑:在 __exit__ 里又抛出了异常。这会导致原始异常被覆盖,调试时根本找不到真正的问题。记住:__exit__ 里尽量只做清理工作,不要做复杂的业务逻辑。
3.7 知识体系图
下面这张图帮你理清上下文管理器的核心脉络:
3.8 总结
上下文管理器是 Python 里一个非常优雅的设计。它把「获取资源-使用资源-释放资源」这个固定模式封装起来,让你不用再写一堆 try/finally 的样板代码。
我个人觉得,掌握好上下文管理器,是区分 Python 新手和老手的一个标志。新手可能只会用 with open,而老手会自己写上下文管理器来封装各种资源操作。
记住三个要点:
- 用 with 语句替代手动 close
- 用 __enter__/__exit__ 或 @contextmanager 实现自定义管理器
- __exit__ 里只做清理,不要抛异常
好了,这一章就到这里。代码写多了,你会发现上下文管理器真的是 Python 里最贴心的设计之一。