1. 什么是上下文管理器? #
with语句用于自动管理资源:进入时做准备工作,退出时(无论正常还是异常)做清理工作。- 最常见的例子是
with open(...)打开文件,退出with块时自动关闭,无需手动f.close()。 - 上下文管理器是实现了
__enter__和__exit__方法的对象,open()返回的文件对象就是其一。 - 不用
with时需要try/finally确保清理,容易遗漏,with是更安全的写法。
# 以只读模式打开文件,退出 with 时自动关闭
with open("file.txt", "r", encoding="utf-8") as f:
# 读取文件全部内容
content = f.read()
# 文件已自动关闭2. contextlib 模块 #
contextlib是 Python 标准库,用于简化上下文管理器的编写,无需手写__enter__和__exit__。- 项目中最常用的是
@contextmanager装饰器,用生成器函数快速定义进入/退出逻辑。
3. @contextmanager 基本用法 #
- 用
@contextmanager装饰一个生成器函数,yield之前的代码是进入逻辑,yield之后是退出逻辑。 yield后面的值会传给with ... as 变量,没有值时写yield即可。- 清理代码必须放在
try/finally中,确保with块内发生异常时也会执行。 - 生成器里只能
yield一次,这是@contextmanager与普通生成器的核心区别。
# 从 contextlib 导入 contextmanager 装饰器
from contextlib import contextmanager
# 用装饰器将生成器函数转为上下文管理器
@contextmanager
def my_context():
# 进入 with 时执行的准备工作
print("进入 with,做准备工作")
try:
# 将值传给 as 后面的变量
yield "你好"
finally:
# 退出 with 时执行的清理工作(无论是否异常)
print("退出 with,做清理工作")
# 使用自定义上下文管理器,msg 接收 yield 的值
with my_context() as msg:
print("在 with 块内:", msg)执行顺序:
进入 with,做准备工作
在 with 块内: 你好
退出 with,做清理工作4. 实用示例:计时器 #
- 用
@contextmanager封装计时逻辑是项目中的常见模式,可测量代码块执行耗时。 - 进入时记录
time.perf_counter(),退出时在finally中计算差值并打印。 - 类似写法还可用于:临时切换配置、开启/关闭日志级别、获取/释放资源等。
- 比在每个函数里手写计时代码更干净,且保证无论是否异常都会输出耗时。
# 导入 contextmanager 装饰器
from contextlib import contextmanager
# 导入 time 模块用于高精度计时
import time
# 定义带可选标签的计时器上下文管理器
@contextmanager
def timer(label: str = ""):
# 记录进入 with 时的时间戳
start = time.perf_counter()
try:
# 将控制权交给 with 块内的代码
yield
finally:
# 计算耗时(秒)
elapsed = time.perf_counter() - start
# 有标签时添加前缀,否则为空字符串
prefix = f"[{label}] " if label else ""
# 打印耗时,保留三位小数
print(f"{prefix}耗时: {elapsed:.3f} 秒")
# 在计时器上下文中执行业务逻辑
with timer("数据处理"):
# 模拟耗时操作
time.sleep(0.5)
print("执行业务逻辑...")5. closing:自动关闭资源 #
- 有些对象有
close()方法但不是上下文管理器,closing()包装后可在with中自动关闭。 - 适合数据库连接、网络连接、自定义资源等需要显式关闭的对象。
- 退出
with块时无论是否异常,都会调用close(),与with open()的行为一致。 - 如果对象本身已支持
with(实现了__enter__/__exit__),则无需closing()。
# 导入 closing,为有 close() 方法的对象提供 with 支持
from contextlib import closing
# 模拟一个需要手动关闭的资源类
class SimpleResource:
def __init__(self):
print("资源已打开")
def close(self):
print("资源已关闭")
# 退出 with 时自动调用 close()
with closing(SimpleResource()):
print("正在使用资源...")
# 自动调用 close()6. suppress:忽略指定异常 #
suppress(异常类型)在with块内捕获并静默忽略指定异常,块外程序正常继续。- 适合"操作可能失败但不影响主流程"的场景,如删除可能不存在的文件、读取可选配置等。
- 可同时忽略多种异常:
suppress(FileNotFoundError, PermissionError)。 - 只忽略列出的异常,其他异常仍会正常抛出,不会掩盖真正的错误。
# 导入 suppress,用于静默忽略指定异常
from contextlib import suppress
# 导入 os 模块进行文件操作
import os
# 等价于 try/except FileNotFoundError: pass,但更简洁
with suppress(FileNotFoundError):
# 删除可能不存在的文件,不存在时不报错
os.remove("可能不存在的文件.txt")
# 无论文件是否存在,程序都会继续执行
print("程序继续执行")7. 注意事项 #
@contextmanager中清理代码必须放在finally里,否则异常时不会执行清理。- 生成器函数只能
yield一次,yield前是进入逻辑,yield后是退出逻辑。 suppress不要滥用,只用于确实可以安全忽略的异常,避免隐藏真正的 bug。threading.Lock本身已支持with lock:,一般不需要用@contextmanager再封装。
8. 总结 #
contextlib让你用更少的代码编写上下文管理器,核心是@contextmanager装饰器。- 日常开发记住三个工具:
@contextmanager(自定义)、closing()(自动 close)、suppress()(忽略异常)。 - 任何需要"进入时准备、退出时清理"的场景,优先考虑
with+contextlib,比try/finally更清晰。 - 文件操作直接用
with open(),无需 contextlib; - 清理逻辑放
finally,确保异常时也能执行 - 计时、临时配置切换等横切逻辑用
@contextmanager封装复用 - 可选操作(文件可能不存在)用
suppress,关键逻辑不要用suppress掩盖错误