《Python编程入门》7.3 上下文管理器与 with

本节讲 Python 的上下文管理器与 with 语句。先说明 with 解决的资源成对获取与释放问题,再拆解上下文管理协议的两个特殊方法、退出方法的三个参数,以及返回真值即吞掉异常的机制;随后用 contextlib 的装饰器把生成器改造成上下文管理器,介绍抑制异常与动态管理多个资源两个工具;最后给出 with 管理多资源时的等价展开,并解释文件、锁、数据库连接为何都该做成上下文管理器。

本节目标:理解 with 语句背后的上下文管理协议,学会用类或 contextlib 编写自己的上下文管理器,掌握 suppress 与 ExitStack 两个实用工具,并弄清 with 多资源写法与等价展开。
适用版本:Python 3.12+(实测 3.14.6)

7.3 上下文管理器与 with

上一节我们把异常处理讲透了。但有一类问题异常单独解决不了:资源必须「用完就还」——文件要关闭、锁要释放、数据库连接要归还连接池。如果中途抛异常,关闭代码就可能被跳过,资源泄漏。try/finally 能解决,但写起来啰嗦。with 语句就是 Python 为此提供的专门语法。

7.3.1 没有 with 的日子

先看用 try/finally 手动管理资源的写法:

def read_config(path):
    f = open(path, encoding="utf-8")
    try:
        return f.read()
    finally:
        f.close()          # 无论是否抛异常,都会执行

这段代码是对的,但每开一个资源就要配一套 try/finally,嵌套两层就是两层缩进。更糟的是容易漏写——打开文件、加锁、连数据库,只要有一处忘了配 finally,泄漏就埋下了。

with 把「获取—使用—释放」这个固定模式收进一个语法块:

def read_config(path):
    with open(path, encoding="utf-8") as f:
        return f.read()

文件在离开 with 块时保证被关闭,无论正常返回还是抛异常。这就是上下文管理器存在的意义。

7.3.2 上下文管理协议:__enter__ 与 __exit__

with 能作用在任何实现了上下文管理协议的对象上——也就是定义了 __enter__ 和 __exit__ 两个方法的对象。

class Managed:
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        print(f"enter {self.name}")
        return self.name.upper()          # 返回值绑定给 as 后面的变量

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"exit {self.name}: exc_type={exc_type}")
        return False                      # 不吞异常

with Managed("res") as v:
    print("body, value =", v)
# 输出:
# enter res
# body, value = RES
# exit res: exc_type=None

执行顺序一目了然:

  1. 求值 with 后面的表达式,得到上下文管理器对象。
  2. 调用它的 __enter__(),把返回值绑定给 as 后面的变量。
  3. 执行 with 块主体。
  4. 无论主体是否抛异常,都调用 __exit__(...)。

注意 __enter__ 的返回值和对象本身是两回事:open() 返回的文件对象,它的 __enter__ 返回的就是文件对象自己,所以 as f 拿到的是文件。但这不是强制的——__enter__ 完全可以返回别的值(比如一个连接的游标)。

7.3.3 __exit__ 的三个参数与「返回真值即吞掉异常」

__exit__ 收到三个参数,分别描述 with 块里发生的异常:

参数含义无异常时
exc_type异常类None
exc_val异常实例None
exc_tbtraceback 对象None

关键规则:如果 __exit__ 返回一个真值,异常就被「吞掉」,with 之后的代码继续执行;返回假值(None / False)则异常继续向外传播。

class Swallow:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("exit, swallowing:", exc_type.__name__ if exc_type else None)
        return True                       # 真值 → 吞掉异常

with Swallow():
    raise ValueError("this will be swallowed")
print("after with, still running")
# 输出:
# exit, swallowing: ValueError
# after with, still running

raise ValueError 没有让程序崩溃,因为 __exit__ 返回了 True。

绝大多数情况下 __exit__ 应该返回假值(或干脆不写 return)。吞异常是极少数场景才用的能力,比如 contextlib.suppress 就是刻意利用这个机制实现的。一个容易忽视的细节:如果 with 块里的异常类型不在 __exit__ 能处理的范围内,那就该老老实实返回假值让它传出去。

还有一点:如果 __enter__ 自己抛异常,__exit__ 不会被调用(因为资源压根没获取成功)。所以 __enter__ 内部也要保证要么成功、要么不留半成品。

7.3.4 contextlib.contextmanager:用生成器写上下文管理器

为一个简单的资源写一个完整的类,样板代码太多。contextlib.contextmanager 允许你用一个生成器函数来表达上下文管理器:yield 之前是「获取」,yield 的值就是 as 拿到的对象,yield 之后是「释放」。

import time
from contextlib import contextmanager

@contextmanager
def timer(label):
    start = time.perf_counter()
    try:
        yield label                       # yield 的值绑定给 as 变量
    finally:
        print(f"{label}: {time.perf_counter() - start:.4f}s")

with timer("work") as label:
    print("inside", label)
# 输出:
# inside work
# work: 0.0000s

要点:

  • yield 前是 __enter__,yield 后是 __exit__。
  • 异常会被「抛回」yield 那一行。也就是说,如果 with 块里出错,异常会在生成器的 yield 处重新抛出,你可以在那里 try/except 处理。
  • 必须用 try/finally 包住 yield,否则 with 块抛异常时释放逻辑不会执行。

看一个把异常「接住再放行」的例子:

@contextmanager
def managed():
    print("acquire")
    try:
        yield "res"
    except ValueError as e:
        print("handle in cm:", e)
        raise                             # 处理完仍然抛出
    finally:
        print("release")

try:
    with managed() as r:
        print("use", r)
        raise ValueError("oops")
except ValueError as e:
    print("outer:", e)
acquire
use res
handle in cm: oops
release
outer: oops

注意 release 在 outer 之前打印——finally 先执行,异常随后才传到最外层。这个顺序和上一节讲的 finally 语义完全一致。

7.3.5 contextlib.suppress:优雅地忽略指定异常

有些异常你明确知道「忽略即可」,比如尝试删除一个可能不存在的文件。上一节说过别写裸 except:,suppress 提供了精确的替代:

from contextlib import suppress

with suppress(FileNotFoundError):
    open("/nonexistent/path/xyz")
print("no error escaped")          # no error escaped

它等价于一段 try/except FileNotFoundError: pass,但更短也更不容易写错。suppress 也可以传多个异常类型:with suppress(FileNotFoundError, PermissionError):。但它只该用于「确实无关紧要」的情况——如果异常背后可能有真问题,抑制它只会让 bug 更难查。

7.3.6 contextlib.ExitStack:动态管理多个资源

with 的静态写法要求你在写代码时就知道要开几个资源。但有时数量是运行期才知道的——比如「打开一个目录下所有文件」。ExitStack 让你把资源一个一个压入栈,离开 with 时按后进先出(LIFO)顺序统一释放。

from contextlib import ExitStack
import tempfile, os

with tempfile.TemporaryDirectory() as d:
    paths = [os.path.join(d, f"f{i}.txt") for i in range(3)]
    with ExitStack() as stack:
        files = [stack.enter_context(open(p, "w")) for p in paths]
        for i, f in enumerate(files):
            f.write(f"line {i}\n")
        print("opened:", len(files), "files")              # opened: 3 files
    print("all closed:", all(f.closed for f in files))      # all closed: True

ExitStack 的两个核心方法:

  • enter_context(cm):调用 cm.__enter__(),把 __exit__ 注册进栈,返回 __enter__ 的结果。
  • callback(fn, *args):注册一个普通函数,退出时调用。

退出顺序是严格的 LIFO(后注册的先执行):

with ExitStack() as stack:
    stack.callback(lambda: print("callback 1"))
    stack.callback(lambda: print("callback 2"))
    print("body")
# 输出:body → callback 2 → callback 1

LIFO 不是随意选的:资源之间常有依赖关系(后开的可能依赖先开的),先释放后来的、再释放早来的,才能保证释放时不引用已销毁的东西。用 try/finally 手写嵌套也是同样的顺序。

ExitStack 特别适合动态资源 + 可选资源的场景:把「需要时才创建」的资源在运行时压栈,无论后续哪一步失败,已创建的全部会被清理。它还是写装饰器、上下文管理器组合工具的基础设施。

7.3.7 多资源 with 与等价展开

with 可以一次管理多个资源,用逗号分隔:

with open("a.txt", "w") as fa, open("b.txt", "w") as fb:
    fa.write("A")
    fb.write("B")

它等价于把两个 with 嵌套起来。而单个 with 又等价于下面这段 try/finally——这就是 with 的完整展开,理解了它,就理解了 with 的一切:

mgr = open("a.txt", "w")
enter = type(mgr).__enter__               # 注意:在类型上查找,跳过实例属性
exit_ = type(mgr).__exit__
fa = enter(mgr)                           # __enter__ 的返回值
try:
    fa.write("A")
except BaseException:
    if not exit_(mgr, *sys.exc_info()):   # __exit__ 返回假值 → 异常继续抛
        raise
else:
    exit_(mgr, None, None, None)          # 无异常,参数全是 None

几个从展开里能看出来的关键点:

  • 异常穿透时 __exit__ 收到的是真实的异常三元组,返回假值就 raise 原异常。
  • 正常结束时 __exit__ 收到三个 None。
  • 查找 __enter__ / __exit__ 用的是类型而不是实例,所以给实例动态挂方法是不生效的。

Python 3.10 起,with 还支持带括号的多行写法,用 with ( 起头、每个资源一行、) 收尾,长资源列表更易读。

7.3.8 为什么文件、锁、连接都做成上下文管理器

回头看「上下文管理器」的本质:它把「成对的进入与退出」抽象成一个可复用的协议。 凡是具备这个形状的资源,都适合做成上下文管理器:

资源进入退出
文件open()close()
线程锁acquire()release()
数据库连接从连接池取出归还连接池 / 回滚未提交事务
临时目录创建递归删除
计时器记录起始时间打印/上报耗时

以锁为例,threading.Lock 本身就是上下文管理器:

import threading

lock = threading.Lock()
with lock:
    print("locked, locked() =", lock.locked())
print("after with, locked() =", lock.locked())
locked, locked() = True
after with, locked() = False

如果不用 with,就得在 acquire() 之后配一个 finally: lock.release();一旦中间有 return 或异常,漏掉释放就死锁。上下文管理器的价值就是把「配对」变成语言层面的保证,让你不可能忘记释放。

自己实现时,一个常见的增强是让 __exit__ 处理事务语义——比如数据库连接在异常时回滚、正常时提交:

class Transaction:
    def __init__(self, conn):
        self.conn = conn

    def __enter__(self):
        self.conn.begin()
        return self.conn

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.conn.rollback() if exc_type else self.conn.commit()
        return False                      # 不吞异常,让调用方知道失败了

__exit__ 拿到了 exc_type,就能根据「有没有出错」决定提交还是回滚——这正是它比 try/finally 更强的地方:释放逻辑可以感知异常。

7.3.9 一句话预告 async with

异步世界里有一个对应的协议:__aenter__ / __aexit__,配合同样是异步的 async with 使用,语法形状完全一致:

async with aiohttp.ClientSession() as session:   # 第 13 章展开
    ...

它解决的是同一个问题——只是「进入」和「退出」本身也变成了需要 await 的异步操作。第 13 章会结合事件循环详细讲。

小结

  • with 把「获取—使用—释放」固定模式收进一个语法块,保证资源一定被释放,即使中途抛异常。
  • 上下文管理协议 = __enter__(返回值绑定给 as 变量)+ __exit__(exc_type, exc_val, exc_tb)。
  • __exit__ 返回真值即吞掉异常,返回假值则异常继续传播;多数情况应返回假值。
  • __enter__ 抛异常时 __exit__ 不会被调用。
  • @contextmanager 把生成器变成上下文管理器:yield 前是进入、yield 后是退出,异常会抛回 yield 处,务必用 try/finally 包住 yield。
  • suppress 精确忽略指定异常;ExitStack 在运行期动态压入多个资源,退出时按 LIFO 统一释放。
  • with A as a, B as b 等价于嵌套 with,而单个 with 展开成 try/finally;Python 3.10 起支持带括号的多行写法。
  • 文件、锁、连接、临时目录都适合做成上下文管理器;__exit__ 能感知异常,可据此实现提交/回滚等事务语义。
  • 异步版本是 __aenter__ / __aexit__ 与 async with,第 13 章展开。

到这里第 7 章就结束了:我们讲了异常如何表达错误、如何设计错误类型、如何用 with 管理资源。下一章进入 Python 最优雅的机制之一——迭代协议与生成器:for 循环背后发生了什么,yield 如何让函数「暂停」,以及如何用生成器写出惰性、省内存的数据管道。

阅读导航:上一节:7.2 自定义异常、异常链与错误设计 · 下一节:8.1 可迭代协议、迭代器与生成器 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. 《Python高级编程》目录
  2. 《Python高级编程》11.3 PEP 流程与版本迁移策略
  3. 《Python高级编程》11.2 嵌入式与自由线程运行时