上下文管理器
程序中大量资源需要成对操作:打开后必须关闭、加锁后必须解锁、连接后必须断开。手动管理这些资源极易因异常或逻辑分支导致泄漏。Python 的 with 语句通过上下文管理器协议,将资源获取与释放绑定为不可分割的原子操作,确保无论代码块正常结束还是异常抛出,清理逻辑始终执行。
with 语句的本质
with 语句的语法为 with expression [as variable]:,其中 expression 必须返回一个实现了上下文管理器协议的对象。进入 with 块时,协议启动资源;离开块时,协议关闭资源。
以文件为例,不使用 with 时,异常会跳过 close() 调用,导致文件句柄泄漏:
f = open('data.txt', 'r', encoding='utf-8')
try:
data = f.read()
# 若此处抛出异常,close() 永远不会执行
finally:
f.close()
with 语句将上述 try-finally 结构压缩为一行,语义等价但更安全、更简洁:
with open('data.txt', 'r', encoding='utf-8') as f:
data = f.read()
# 离开 with 块后,f.closed 为 True
as f 中的 f 接收的是上下文管理器 __enter__ 方法的返回值,不一定是管理器对象本身。对于文件对象,__enter__ 返回 self,因此 f 就是文件对象。
上下文管理器协议
任何类只要实现以下两个特殊方法,即可作为上下文管理器使用:
__enter__(self):进入with块时调用,负责资源初始化,返回值绑定到as后的变量。__exit__(self, exc_type, exc_val, exc_tb):离开with块时调用,负责资源清理。三个参数描述块内发生的异常(若无异常则均为None)。
__exit__ 的返回值决定异常是否继续传播:返回 True 表示异常已被处理,不再向上抛出;返回 False 或 None 则让异常正常传播。
class DatabaseConnection:
def __enter__(self):
self.connected = True
print('连接数据库')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.connected = False
print('关闭数据库')
if exc_type is not None:
print(f'块内发生异常:{exc_val}')
return False # 不吞掉异常,继续抛出
def query(self, sql):
if not self.connected:
raise RuntimeError('未连接')
print(f'执行:{sql}')
# 正常使用
with DatabaseConnection() as db:
db.query('SELECT * FROM users')
# 离开块后自动关闭
# 异常场景
with DatabaseConnection() as db:
db.query('SELECT *')
raise ValueError('查询参数错误') # __exit__ 仍会执行,异常继续抛出
若 __enter__ 自身抛出异常,with 块不会进入,__exit__ 也不会调用。若 __exit__ 需要处理特定异常类型,可检查 exc_type:
class SuppressZeroDivision:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is ZeroDivisionError:
print('捕获除零错误,静默处理')
return True # 吞掉异常
return False
with SuppressZeroDivision():
print(1 / 0) # 不会崩溃,输出提示后正常继续
print('程序继续运行')
contextlib 模块
对于简单的资源管理,编写完整类显得笨重。contextlib 模块提供了更轻量的构建方式。
contextmanager 装饰器
用生成器函数配合 @contextmanager,yield 之前的代码等价于 __enter__,yield 的值绑定到 as 变量,yield 之后的代码等价于 __exit__(无论是否异常都会执行,因为被包装在 try-finally 中)。
from contextlib import contextmanager
@contextmanager
def managed_file(path, mode='r', encoding='utf-8'):
f = open(path, mode, encoding=encoding)
try:
yield f
finally:
f.close()
print(f'文件 {path} 已关闭')
with managed_file('test.txt', 'w') as f:
f.write('hello')
生成器写法中,若 yield 之前的代码抛出异常,不会进入 with 块;若块内抛出异常,会在生成器中 yield 处重新抛出,因此 yield 通常应放在 try 块内,确保 finally 能执行清理。
其他实用工具
closing(thing):为仅实现了close()但没有__enter__/__exit__的对象(如某些第三方库句柄)快速包裹上下文管理器。
from contextlib import closing
from urllib.request import urlopen
with closing(urlopen('https://www.example.com')) as page:
print(page.read(100))
suppress(*exceptions):静默忽略指定异常,等价于一个自动吞掉特定异常的上下文管理器。
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove('maybe_not_exist.tmp') # 文件不存在也不报错
redirect_stdout(new_target)/redirect_stderr(new_target):临时重定向标准输出/错误流到文件或其他文件对象。
from contextlib import redirect_stdout
import io
f = io.StringIO()
with redirect_stdout(f):
print('这条信息被捕获')
print(f.getvalue()) # '这条信息被捕获\n'
ExitStack:动态管理数量不确定的上下文管理器,在复杂场景下替代嵌套的with。
from contextlib import ExitStack
filenames = ['a.txt', 'b.txt', 'c.txt']
with ExitStack() as stack:
files = [stack.enter_context(open(name, 'r')) for name in filenames]
# 所有文件在 ExitStack 退出时统一关闭
contents = [f.read() for f in files]
多个上下文管理器
Python 支持在单个 with 语句中并列多个上下文管理器,用逗号分隔。执行顺序是:从左到右依次调用 __enter__,代码块结束后从右到左依次调用 __exit__。这种后进先出的顺序与嵌套 with 完全一致。
with open('src.txt', 'r', encoding='utf-8') as src, \
open('dst.txt', 'w', encoding='utf-8') as dst:
dst.write(src.read().upper())
等价的嵌套写法:
with open('src.txt', 'r', encoding='utf-8') as src:
with open('dst.txt', 'w', encoding='utf-8') as dst:
dst.write(src.read().upper())
并列写法更紧凑,适合管理多个同类资源。若某个 __enter__ 抛出异常,之前已成功进入的管理器会按相反顺序调用 __exit__,保证已获取的资源被正确释放。
实际应用场景
上下文管理器不仅用于文件,凡是需要成对操作的场景都适用:
- 数据库事务:进入时开启事务,正常结束时提交,异常时回滚。
- 线程/进程锁:
with lock:确保临界区结束后自动释放锁。 - 临时环境变更:修改
sys.path、切换工作目录、设置环境变量,退出时恢复原状。 - 性能计时:进入记录开始时间,退出计算并打印耗时。
import time
from contextlib import contextmanager
@contextmanager
def timer(name):
start = time.perf_counter()
yield
elapsed = time.perf_counter() - start
print(f'{name} 耗时: {elapsed:.4f} 秒')
with timer('数据处理'):
sum(range(1000000))
总结
上下文管理器通过 __enter__ 和 __exit__ 两个方法,将资源生命周期与代码块作用域绑定。with 语句是调用方,@contextmanager 是轻量实现方,contextlib 中的 closing、suppress、redirect_stdout、ExitStack 等工具覆盖了常见变体。多个管理器并列时遵循后进先出原则。掌握这一协议,就能在文件、网络、锁、事务等任何资源场景中写出异常安全的代码。