>>>PyPathPython 学习站
首页›进阶篇›kp-025
进阶篇 · Advanced lv.3 进阶 kp-025

上下文管理器(with)

前置知识:kp-016、kp-019

1. 一句话定义

with 表达"进入 → 使用 → 无论成败都清理"的确定性资源管理:对象实现 __enter__/__exit__ 即可被 with 使用;临时行为可用 contextlib 三行搞定。

2. 为什么重要

try/finally 的结构化替代:文件、锁、数据库连接、临时目录、计时段——一切"必须成对出现的操作"都该长成 with。Python 3.12 的单文件 __init__ 生成器让这类代码更省。

3. 前置知识

kp-016(with open 的表层用法)、kp-019(协议)。

4. 核心概念

python
# ① 协议类
class Timer:
    def __enter__(self):
        self.t0 = time.perf_counter()
        return self                      # as 变量拿到的是 __enter__ 的返回值
    def __exit__(self, exc_type, exc, tb):
        print(f"耗时 {time.perf_counter()-self.t0:.3f}s")
        return False                     # False/None:异常继续传播;True:吞掉

with Timer():
    heavy()

# ② contextlib:临时上下文的最小成本写法
from contextlib import contextmanager

@contextmanager
def timer():
    t0 = time.perf_counter()
    try:
        yield                            # yield 之前 = __enter__,之后 = __exit__
    finally:
        print(f"耗时 {time.perf_counter()-t0:.3f}s")

# ③ 标准库常用工具
import contextlib, tempfile
with contextlib.suppress(FileNotFoundError):   # 静默特定异常
    os.remove("tmp.txt")
with open("a") as fa, open("b") as fb: ...     # 多资源,3.1+ 可写括号组
with contextlib.ExitStack() as stack:          # 动态数量的资源
    files = [stack.enter_context(open(p)) for p in paths]

5. 原理与机制

text
with obj as x:
    body
等价于:
  mgr = obj
  x = mgr.__enter__()
  try:
      body
  except BaseException as e:
      if not mgr.__exit__(type(e), e, e.__traceback__): raise
  else:
      mgr.__exit__(None, None, None)

__exit__ 返回真值即"已处理"(吞异常)——能力强大但慎用,默认 return False。

6. 关键事实(模型/图示)

text
进入 with ──► __enter__(可返回值给 as)
退出 with ──► __exit__ 必被调用(正常/异常都算"退出")
生成器版:yield 前后的代码天然对应 enter/exit

7. 直观类比

with 是手术室的进出规程:进门(enter)自动更衣消毒,出门(exit)无论手术成功失败都自动脱衣销毁器械——规程(协议)保证"进去过的人一定按流程出来",没有"忘了"这个状态。

8. 实例与案例

python
# 数据库事务模板
@contextmanager
def transaction(conn):
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise                            # 回滚后继续抛出,调用方知情

9. 常见误区

  1. __exit__ 里吞异常忘返回值 —— 返回 None 等于 False(不吞);想要"处理且吞掉"必须显式 return True。
  2. 以为 with 能管理任意对象 —— 没实现协议的对象进 with 直接 AttributeError;临时写法用 contextlib。
  3. 在 __exit__ 里再抛新异常 —— 会与原异常链交织,极难排查;清理代码应只清理。
  4. 嵌套深了还手动 with 缩进 —— 资源数量动态时用 ExitStack。

10. 自测题

  1. with obj as x: 中 x 是什么?谁把它赋给 x?
  2. @contextmanager 装饰的生成器里,yield 前后各对应什么?抛出异常会走哪段?
  3. 什么时候必须用 ExitStack 而不是多个 with?
参考答案
  1. __enter__ 的返回值;with 语句机制自动绑定。
  2. yield 前 = 进入逻辑,yield 后 = 退出逻辑;进入与使用中抛出的异常都会在 yield 处重新抛出,落入 finally/except 段。
  3. 资源集合在运行期才确定(列表里有多少就开多少个)时。

11. 与其他知识点的关系

  • kp-015 异常处理:with 是"清理"维度的专用语法。
  • kp-024 装饰器:同为"包裹执行",一个作用于定义期、一个作用于调用期。
  • kp-031 asyncio:异步上下文 async with 的同步版原理。

12. 延伸阅读

  • PEP 343(with 语句)
  • contextlib 文档:https://docs.python.org/zh-cn/3/library/contextlib.html