小白学 Python
AI 编程指南GitHub
© 2026 小白学 Python · 基于 walter201230/Python 教程
课程目录关于本站联系方式隐私政策GitHub

上下文管理器(with)

完整讲解 · 5 段教学·配 4 道练习题·预计 30 分钟

本页是本章的通读版,可直接读完全部讲解。想动手写代码、跑判分,去 闯关模式。

教学 01 / 05

上下文管理器:with 到底在做什么

各位先来看一段代码,猜猜哪里有坑:

python到闯关页运行这段 →
f = open('/tmp/diary.txt', 'w')
f.write('今天打卡迟到了三分钟。')
f.close()

「这有什么问题?文件打开了、写完了、关掉了,一气呵成。」善于思考的你可能会这么说。

可是,万一 f.write 那一行抛了异常呢?

close 永远不会被执行,文件描述符就这么悬在那儿。一两次没事,可如果是个 Web 服务跑一整天,几千个请求里只要有几十个写文件失败,操作系统的 fd 就会被慢慢耗光,最后报一个看起来八竿子打不着的「Too many open files」。这种 bug,定位起来非常痛苦。

那怎么办?老办法当然是 try/finally:

python到闯关页运行这段 →
f = open('/tmp/diary.txt', 'w')
try:
    f.write('今天打卡迟到了三分钟。')
finally:
    f.close()

但这样很啰嗦。Python 早就提供了一个语法糖:

python到闯关页运行这段 →
with open('/tmp/diary.txt', 'w') as f:
    f.write('今天打卡迟到了三分钟。')

一行顶六行,且保证不管中间是否抛异常,文件都会被关掉。

这就是 with 语句——它背后的机制叫上下文管理器(context manager)。

教学 02 / 05

一、上下文管理协议

with 后面跟的不是「文件」也不是「连接」,而是一类东西,Python 给它起了个名字,叫上下文管理器。

什么样的对象能算上下文管理器?只要满足两个方法:

  • __enter__(self):进入 with 块时被调用,返回值赋给 as 后面的变量
  • __exit__(self, exc_type, exc_val, tb):离开 with 块时被调用——无论是正常离开还是异常离开

这就是「上下文管理协议」。它的全部规则就这两条,没有第三条。

一个最简单的例子

python到闯关页运行这段 →
class Punch:
    def __enter__(self):
        print('进入打卡区')
        return self

    def __exit__(self, exc_type, exc_val, tb):
        print('离开打卡区')


with Punch() as p:
    print('正在工位摸鱼')

输出:

进入打卡区
正在工位摸鱼
离开打卡区

with Punch() as p 这一行做了三件事:

  1. 创建一个 Punch() 实例
  2. 调用这个实例的 __enter__() 方法
  3. 把 __enter__() 的返回值赋给 p

然后才执行 with 块里面的代码。等 with 块结束(不管是正常结束还是异常结束),就调用 __exit__()。

这个流程本质上就是把:

python到闯关页运行这段 →
p_obj = Punch()
p = p_obj.__enter__()
try:
    print('正在工位摸鱼')
finally:
    p_obj.__exit__(None, None, None)

这一坨样板代码,藏到了 with 这个语法糖背后。

练习 1 / 4用 with 操作 StringIO去闯关页做这题 →
教学 03 / 05

二、自己写一个上下文管理器

光是 print 几行字太单薄了,我们让上下文管理器做点正经事——比如记录进入和离开的事件:

python到闯关页运行这段 →
class PunchLog:
    def __init__(self, name):
        self.name = name
        self.records = []

    def __enter__(self):
        self.records.append(f'{self.name} 进入工位')
        return self

    def __exit__(self, exc_type, exc_val, tb):
        self.records.append(f'{self.name} 离开工位')


log = PunchLog('两点水')
with log:
    print('正在划水……')

for line in log.records:
    print(line)

输出:

正在划水……
两点水 进入工位
两点水 离开工位

⚠️ 常见坑:如果改成 with log as l:,那 l 会变成 None,因为 __enter__ 没写 return self,默认返回 None。「我的 as x 怎么是 None?」八成是 __enter__ 忘了 return。

exit 的三个参数:异常处理

如果 with 块里抛了异常,__exit__ 会拿到三个参数:

  • exc_type:异常类型
  • exc_val:异常实例
  • tb:traceback 对象
python到闯关页运行这段 →
class CatchAll:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, tb):
        if exc_type is not None:
            print(f'捕获到异常:{exc_type.__name__}: {exc_val}')
        return True   # 返回 True 表示「这个异常我吞掉了」


with CatchAll():
    raise ValueError('故意的')

print('with 之后的代码继续执行')

输出:

捕获到异常:ValueError: 故意的
with 之后的代码继续执行

__exit__ 返回 True 表示吞掉异常——异常不会再向外传播。返回 False(或 None)表示让异常继续往上抛。

练习 2 / 4自定义上下文管理器类去闯关页做这题 →
教学 04 / 05

三、@contextmanager:用生成器写上下文管理器

写一个上下文管理器,只为了管几行代码,居然要写一个完整的类 + 两个 dunder 方法,是不是有点重?

Python 标准库 contextlib 提供了一个装饰器 @contextmanager,可以把一个生成器函数变成上下文管理器:

  • yield 之前的代码 = __enter__
  • yield 出来的值 = __enter__ 的返回值
  • yield 之后的代码 = __exit__

把上一节的 PunchLog 用 @contextmanager 重写:

python到闯关页运行这段 →
from contextlib import contextmanager


@contextmanager
def punch_log(name):
    records = []
    records.append(f'{name} 进入工位')
    try:
        yield records
    finally:
        records.append(f'{name} 离开工位')


with punch_log('两点水') as records:
    print('继续划水……')

for line in records:
    print(line)

输出:

继续划水……
两点水 进入工位
两点水 离开工位

是不是清爽多了?类变成了一个函数,两个 dunder 方法变成了一个 yield。

重点:清理代码必须包在 finally 里

注意上面那个 try/finally。为什么需要它?

因为 with 块里面可能抛异常,异常会从 yield 那一行重新「钻」回生成器函数里。如果不写 try/finally,异常会绕过后面的清理代码,资源就泄露了。

这是用 @contextmanager 写上下文管理器最重要的一条铁律:清理代码要包在 finally 里。

用 try/except 吞掉异常

如果想吞掉某种特定异常:

python到闯关页运行这段 →
from contextlib import contextmanager


@contextmanager
def ignore_zero_division():
    try:
        yield
    except ZeroDivisionError as e:
        print(f'吞掉了:{e}')


with ignore_zero_division():
    x = 1 / 0
    print('这一行不会执行')

print('但是这一行会')

输出:

吞掉了:division by zero
但是这一行会
练习 3 / 4用 @contextmanager 写一个 banner去闯关页做这题 →
教学 05 / 05

四、contextlib 里的几个常用工具

contextlib 不只有 @contextmanager,还有几个开箱即用的小工具,用过几次之后会觉得离不开。

1、suppress:优雅地忽略某种异常

有时候我们就是想忽略某个无关紧要的错误,老办法是:

python到闯关页运行这段 →
try:
    do_something()
except SomeError:
    pass

写多了很啰嗦。suppress 让它变一行:

python到闯关页运行这段 →
from contextlib import suppress

with suppress(FileNotFoundError):
    import os
    os.remove('not-exist.txt')   # 不存在也不会报错

如果文件不存在,FileNotFoundError 会被静默吞掉;如果是别的异常,照常往外抛。

2、closing:把没实现 with 的对象包成上下文

不是所有需要 close 的对象都实现了上下文管理器协议。closing 帮我们补上这一层:

python到闯关页运行这段 →
from contextlib import closing

class MyResource:
    def close(self):
        print('资源已释放')


with closing(MyResource()):
    print('使用资源中…')

输出:

使用资源中…
资源已释放

closing 的实现非常简单——它就是确保 with 块结束时调用对象的 .close() 方法。

3、redirect_stdout:临时改写标准输出

有时候你想把 print 的内容暂时收集起来——可以用 redirect_stdout:

python到闯关页运行这段 →
import io
from contextlib import redirect_stdout

buffer = io.StringIO()
with redirect_stdout(buffer):
    print('这一行不会出现在终端')
    print('而是被收进了 buffer')

captured = buffer.getvalue()
print('外面打印捕获到的内容:')
print(captured)

总结

写上下文管理器的三种方式适合什么场景
写类 + __enter__/__exit__资源对象有自己的状态、需要好几个属性
@contextmanager 装饰生成器临时的、一次性的资源管理
contextlib.suppress / closing 等内置标准模式,能用就直接用

凡是「有借有还、成对操作」的事情——开/关、连/断、获取/释放——扔给 with 去管准没错。

练习 4 / 4用 contextlib.suppress 忽略异常去闯关页做这题 →

读完了?动手练一遍才算真会。

去闯关模式练习 →