第17篇|上下文管理器
本篇定位:《Python 教程》专栏二「Python 进阶与工程」第 17 篇。承接第 10 篇的
with open、第 11 篇的try/finally、第 14 篇点到的enter/exit,以及第 15、16 篇的生成器与装饰器,把 上下文管理器(context manager) 讲透:协议心智模型、类式实现、contextlib.contextmanager、异常抑制、可重入、嵌套与ExitStack,以及计时 / 加锁 / 临时目录 /torch.no_grad等工程场景。为第 18 篇类型注解、第 22 篇测试夹具周边、第 23~25 篇并发与异步、第 26~28 篇 HTTP / FastAPI / Agent 打底。读完你将能够:把
with翻译成「enter→ 块体 →exit」;手写类式与@contextmanager两种实现;解释exit三参数与「返回真抑制异常」;用多层with/ExitStack管理固定与动态资源;对照装饰器选择「函数级横切」还是「块级生命周期」;避开「忘记清理」「吞掉异常」「不可重入误用」等坑。预计用时:阅读 35~45 分钟,动手 25~35 分钟。
第 16 篇把「在函数调用前后插入行为」立成装饰器;本篇转向另一把工程刀具——在一段代码块的进入与退出时保证资源获取与清理。文件句柄、线程锁、临时目录、数据库会话、HTTP 客户端、CUDA stream、torch.no_grad(),几乎都是「进去时占住,出来时必须还」的对象。
第 10 篇已经让你养成 with open(...) as f 的肌肉记忆;第 11 篇用 try / finally 保证清理。本篇把二者背后的协议展开:任何实现了 enter / exit 的对象,都能被 with 驱动。你还会看到第 15 篇的生成器如何借 @contextmanager 变成「只暂停一次的资源协议」。
一句话定心:上下文管理器是「可进入、可退出」的对象;with 保证块结束(含异常)时必定调用 exit。 装饰器偏声明时缠绕函数;上下文偏块级生命周期。两者常成对出现:外层 with session,内层 @retry 调 LLM。
建议边读边开练习目录(uv init --no-package)。阅读顺序:协议心智模型 → 手写类式 Timer → @contextmanager → 异常与可重入 → 嵌套 / ExitStack → 标准库与 AI Infra → 踩坑。下一篇预告类型注解与 dataclass、Pydantic(18):资源与横切协议立住后,开始给函数与数据加静态约束。
和第 10 篇对照:with open 是协议的「内置样板」。和第 11 篇对照:exit 相当于标准化的 finally,还可选择抑制异常。和第 14 篇对照:enter / exit 是数据模型的一对插头。和第 15 篇对照:生成器 yield 一次,前后分别对应进入与退出。和第 16 篇对照:@timer 与 with Timer() 都能计时,但一个贴在函数上,一个包住任意语句块。
动手时建议沿用 Timer / chdir / Lock / TemporaryDirectory 意象:先写类式计时器,再改成 @contextmanager,再叠两层 with,最后用 ExitStack 打开路径列表。做完后读 torch.no_grad 或 httpx.Client 的文档,只是换皮。
本篇不穷尽异步上下文(async with / aenter,第 25 篇再补),也不展开描述器与 get 的全部边角。主线是:协议 → 类写法 → 生成器写法 → 组合与抑制 → 工程场景。和「到处手写 try/finally」相比,上下文的价值是:边界清晰、可组合、可读。
读官方文档 With Statement Context Managers 与 contextlib 模块说明时,按本篇目录对照即可。入门阶段把「能写、能读栈、能避坑」做扎实,比收藏冷门上下文库更重要。
写上下文时保持一个工程直觉:清理逻辑写在 exit 或 finally,不要散落在块体中间;能配置的显式传参;需要状态字段用类,只需前后钩子用生成器。 社区里那些「一行 with 搞定」的库很酷,但你自己的业务更需要可读的进入/退出,而不是魔法。
也可以把上下文看成时间维度上的协议:第 15 篇回答「一串值如何跨多次调用产生」;第 16 篇回答「一次调用如何在前后被增强」;本篇回答「一段代码如何在边界处获取与释放资源」。状态与副作用从「散落在业务里」搬到「协议层」——这正是框架喜欢 with 的原因。
写 exit 时再记一条:它不只负责「关资源」,还负责决定异常命运。返回假,异常继续;返回真,异常到此为止。这个开关比 try/except 更隐蔽,所以生产代码里抑制异常必须伴随日志或指标,否则排障时你会以为「根本没出错」。
和「把清理写成注释提醒同事」相比,协议的价值是:编译器与运行时帮你执行提醒——漏写 exit 就不是合法上下文;写了就会在异常路径被调用。框架作者正是靠这一点,才敢在文档里只写一行 with Client() as client:。
若你从第 16 篇刚做完「把 @timer 改成 with Timer()」的热身,本篇前两节会非常眼熟:同样是前后钩子,只是挂钩点从「函数定义」换成了「语句块」。把那份热身代码打开对照着读,迁移成本接近于零。
一、上下文协议:with 到底做了什么
1.1 把 with 翻译成方法调用

图1 上下文协议:enter → 代码块 → exit
下面两段在语义上等价(忽略细节):
with Timer() as t:
work()
mgr = Timer()
t = mgr.__enter__()
try:
work()
finally:
mgr.__exit__(None, None, None) # 有异常时传入类型/值/traceback
要点:
enter** 的返回值**赋给as后面的名字;常返回self,也可返回别的资源对象。- 块体无论正常结束还是抛异常,都会进入
finally调用exit。 exit收到三个参数:exc_type, exc_val, exc_tb;无异常时三者为None。- 若
exit返回真值,异常被抑制,程序继续执行with之后的代码;返回假 /None则异常继续向上抛。
class Demo:
def __enter__(self):
print("enter")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("exit", exc_type)
return False # 不抑制
with Demo():
print("body")
enter
body
exit None
1.2 为什么需要协议,而不是到处 try/finally
try/finally 完全能保证清理,但:
- 每处资源都要写一套样板,容易漏;
- 多个资源嵌套时缩进爆炸;
- 调用方看不到「这东西需要配对释放」的声明。
with 把「进入 / 退出」收成对象的方法。阅读时一眼看见资源边界;实现者把清理写一次,调用者只需 with。
| 手段 | 粒度 | 典型用途 |
|---|---|---|
try/finally | 任意语句 | 一次性、临时清理 |
with 上下文 | 代码块 | 文件、锁、会话、临时状态 |
@装饰器 | 函数调用 | 计时、重试、缓存、注册 |
AI Infra 视角:训练脚本里的
with torch.no_grad():、推理服务里的连接池借还、Agent 工具调用外的会话生命周期,都是同一套「进入 / 退出」插头。先吃透纯 Python 协议,读框架源码只是换名字。
1.3 和第 16 篇装饰器如何分工
- 装饰器:定义时缠绕函数;每次调用都走包装器。适合「这个函数永远需要重试 / 鉴权」。
- 上下文:语句块期间成立;块可以是几行业务,不必是整个函数。适合「这段代码期间关梯度 / 持锁 / 改 cwd」。
同一需求有时两种都能写:@timer 与 with Timer()。选哪个看边界:边界是函数 → 装饰器;边界是任意语句块 → 上下文。
1.4 协议检查:什么样的对象能进 with
运行时并不要求你继承某个基类,只要求对象上能找到 enter 与 exit(鸭子类型)。可用 hasattr 或 isinstance 配合 typing.ContextManager(第 18 篇类型注解会更严格)做检查:
class Ok:
def __enter__(self):
return self
def __exit__(self, *exc):
return False
print(hasattr(Ok(), "__enter__"), hasattr(Ok(), "__exit__"))
True True
缺任一方法,with 会在进入时报错。这比「约定大家都记得 close」可靠得多。
二、手写类式上下文管理器
2.1 最小计时器

图2 类式上下文:手写 Timer
import time
class Timer:
def __enter__(self):
self.t0 = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.dt = time.perf_counter() - self.t0
print(f"took {self.dt:.4f}s")
return False
with Timer() as t:
time.sleep(0.05)
s = sum(range(100_000))
print(f"elapsed attribute: {t.dt:.4f}")
took 0.05xx s
elapsed attribute: 0.05xx
as t 拿到的是 enter 的返回值(这里是 self),所以块外仍可读 t.dt。
2.2 临时切换工作目录
import os
from pathlib import Path
class chdir:
def __init__(self, path):
self.path = Path(path)
self._old = None
def __enter__(self):
self._old = Path.cwd()
os.chdir(self.path)
return self.path
def __exit__(self, *exc):
os.chdir(self._old)
return False
base = Path.cwd()
tmp = base / "_cm_demo"
tmp.mkdir(exist_ok=True)
with chdir(tmp) as p:
assert Path.cwd() == p.resolve()
assert Path.cwd() == base
print("cwd restored")
cwd restored
要点:把旧状态存进实例属性,退出时还原。这是「临时副作用」类上下文的标准形状。
2.3 __exit__ 与异常:默认不吞
class Loud:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("saw", exc_type)
return False # 让异常继续飞
try:
with Loud():
raise ValueError("boom")
except ValueError as e:
print("caught", e)
saw <class 'ValueError'>
caught boom
若写成 return True,ValueError 会被抑制,except 接不到——这在调试时极具迷惑性。默认返回 False / None,除非你明确知道要吞某种异常。
2.4 在 __enter__ 里失败会怎样
若 enter 自己抛异常,不会调用 exit(因为尚未成功进入)。若你在 enter 里已经打开了部分资源,必须自行回滚,或改用 ExitStack 逐步登记:
class BadEnter:
def __enter__(self):
print("trying")
raise RuntimeError("cannot acquire")
def __exit__(self, *exc):
print("exit") # 不会走到
return False
try:
with BadEnter():
pass
except RuntimeError as e:
print("caught", e)
trying
caught cannot acquire
这条规则常被忽略,却是资源泄漏的常见来源:把「获取」拆成多步时,要么全成功,要么自己清理失败现场。
三、@contextmanager:用生成器写上下文
3.1 yield 一次就够

图3 @contextmanager:用生成器写上下文
第 15 篇的生成器「暂停 → 恢复」;contextlib.contextmanager 把它收成:yield 之前当 enter,yield 处把值交给 as,yield 之后(通常在 finally)当 exit。
from contextlib import contextmanager
import time
@contextmanager
def timer(label="block"):
t0 = time.perf_counter()
try:
yield
finally:
print(f"{label}: {time.perf_counter() - t0:.4f}s")
with timer("sum"):
time.sleep(0.03)
sum: 0.03xx s
若要把资源交给 as:
@contextmanager
def opened(path, mode="r", encoding="utf-8"):
f = open(path, mode, encoding=encoding)
try:
yield f
finally:
f.close()
(生产代码仍优先直接 with open;这里只演示形状。)
3.2 异常如何注入生成器
块体里抛出的异常,会经生成器的 throw 注入到 yield 那一行。因此:
@contextmanager
def careful():
print("enter")
try:
yield
except ValueError:
print("handled inside cm")
# 不重新 raise → 相当于抑制该异常
finally:
print("cleanup")
with careful():
raise ValueError("x")
print("after")
enter
handled inside cm
cleanup
after
若 except 里重新 raise,或根本不捕获,异常会继续向外。规则与类式 exit 返回真/假对应:在生成器里「处理且不 re-raise」≈ 返回真抑制。
3.3 类 vs 生成器:怎么选
| 场景 | 推荐 |
|---|---|
| 只需前后钩子、少状态 | @contextmanager |
| 需要多个方法 / 属性 / 可重入计数 | 类 |
| 要做成可继承的库 API | 类 |
| 脚本里临时改 env / cwd | @contextmanager 往往够用 |
3.4 生成器版也要「只 yield 一次」
@contextmanager 要求装饰的生成器函数恰好 yield 一次。yield 零次或两次都会在运行期报错。这和「普通生成器可 yield 多次」不同——这里的生成器只是借用暂停语义表达前后钩子。
from contextlib import contextmanager
@contextmanager
def once():
yield 1
# 不能再 yield
with once() as x:
print(x)
1
若你发现自己想 yield 多次,那说明需求其实是迭代器(第 15 篇),不是上下文管理器。
四、嵌套、ExitStack、可重入
4.1 多层 with 与逗号写法

图4 嵌套 with 与 ExitStack
from pathlib import Path
a = Path("_a.txt")
b = Path("_b.txt")
a.write_text("A", encoding="utf-8")
b.write_text("B", encoding="utf-8")
with open(a, encoding="utf-8") as fa, open(b, encoding="utf-8") as fb:
print(fa.read(), fb.read())
A B
进入顺序:左 → 右(外 → 内);退出顺序:后进先出(先关 fb,再关 fa)。若某一层 enter 失败,已经成功进入的仍会 exit。
4.2 动态个数:ExitStack
路径列表长度运行时才知道时,不能预先写 N 层 with:
from contextlib import ExitStack
from pathlib import Path
paths = [Path("_a.txt"), Path("_b.txt")]
with ExitStack() as stack:
files = [stack.enter_context(open(p, encoding="utf-8")) for p in paths]
print("".join(f.read() for f in files))
# 离开 with 后全部关闭
AB
ExitStack 还可 callback(fn) 注册普通清理函数,适合「不是上下文对象、但离开时要调用」的场景。
4.3 可重入(reentrant)直觉
有的上下文允许同一实例嵌套多次进入,例如 threading.RLock:
import threading
lock = threading.RLock()
with lock:
with lock: # 同一把可重入锁
print("ok")
ok
普通 threading.Lock 不可重入:同一线程再次 acquire 会死锁。自己写的 chdir 若未维护栈式旧路径,嵌套两次也会坏。需要嵌套时,用列表当栈保存旧状态,或每次 with 新建实例。
# 更稳:每次 with chdir(path) 新建对象,而不是复用同一实例嵌套
五、标准库刀具与工程模式
5.1 contextlib 精选
| 工具 | 作用 |
|---|---|
@contextmanager | 生成器 → 上下文 |
ExitStack | 动态组合多个上下文 |
closing(obj) | 离开时调 obj.close() |
nullcontext(x) | 空操作上下文,占位 / 条件分支 |
suppress(*exc) | 抑制指定异常类型 |
redirect_stdout / redirect_stderr | 临时重定向输出 |
chdir(3.11+) | 官方临时改目录 |
from contextlib import nullcontext, suppress
debug = False
cm = open("/dev/null", "w") if debug else nullcontext()
with cm:
print("always runs")
with suppress(FileNotFoundError):
open("_no_such_file_").read()
print("continued")
always runs
continued
nullcontext 在「有时需要真资源、有时不需要」的分支里极有用,避免 if 复制整段业务。
5.2 临时目录
from tempfile import TemporaryDirectory
from pathlib import Path
with TemporaryDirectory() as d:
p = Path(d) / "out.txt"
p.write_text("ckpt", encoding="utf-8")
assert p.exists()
# 目录已被删除
print("temp cleaned")
temp cleaned
测试、下载解压、写中间 checkpoint 再上传,都适合这个形状。
5.3 锁:块级临界区
import threading
counter = 0
lock = threading.Lock()
def bump():
global counter
with lock:
counter += 1
threads = [threading.Thread(target=bump) for _ in range(20)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter)
20
第 23 篇会展开 GIL 与线程;此处只需记住:锁必须用 with,否则异常路径可能永不 release。
5.4 条件性资源:同一套业务两套入口
训练脚本里常见:「调试时把日志 tee 到文件,否则只打 stdout」。用 nullcontext 避免复制业务:
from contextlib import nullcontext, redirect_stdout
from io import StringIO
def run(capture: bool):
buf = StringIO()
cm = redirect_stdout(buf) if capture else nullcontext()
with cm:
print("hello-train")
return buf.getvalue() if capture else None
print(repr(run(True)))
print(run(False))
'hello-train\n'
None
这种「可选上下文」模式在测试与生产切换、是否加锁、是否开 profile 时反复出现,值得练成肌肉记忆。
六、AI Infra 场景:资源边界一目了然

图5 AI Infra 里的上下文:资源与临时状态
6.1 推理时关闭梯度
# 示意:无需真装 torch 也能理解形状
class no_grad:
def __enter__(self):
self._prev = True # 假装读全局开关
# set_grad_enabled(False)
return self
def __exit__(self, *exc):
# set_grad_enabled(self._prev)
return False
with no_grad():
# preds = model(batch)
pass
真实 API 是 with torch.no_grad(): 或 inference_mode()。块级关闭梯度:省显存、加速,且不会误伤块外训练代码。
6.2 HTTP 会话与 Agent 工具循环
from contextlib import contextmanager
@contextmanager
def fake_session():
print("open session")
session = {"client": "httpx"}
try:
yield session
finally:
print("close session")
def call_llm(session, prompt):
return f"[{session['client']}] echo: {prompt}"
with fake_session() as s:
print(call_llm(s, "hi"))
open session
[httpx] echo: hi
close session
第 16 篇的 @retry 可以包在 call_llm 上;本篇的 with session 包住整段多轮工具调用。组合起来:
with session:
for step in agent_loop:
result = retry(call_llm)(...)
6.3 装饰器 vs 上下文:一张对照表
| 需求 | 更合适 |
|---|---|
| 每次调用 LLM 都重试 | @retry |
| 整段 Agent 循环共用一个 Client | with Client() |
| 函数耗时统计 | @timer 或 with Timer() 皆可 |
| 仅某几行关梯度 | with no_grad() |
| 测试里临时改环境变量 | with 改 env + 还原 |
AI Infra 视角:读 vLLM / SGLang / 训练脚本时,先扫
with找出资源与模式开关边界,再跟业务。比从第一行import线性读更快建立心智模型。
6.4 把计时、会话、重试放在同一张图上
想象最小 Agent 一步:
with Session() as s:打开 HTTP 客户端(本篇);@retry(times=3)包住单次call_llm(第 16 篇);with Timer("step"):测量用户可感知延迟(本篇);- 工具结果用
with open(...)落盘(第 10 篇)。
四层边界各管一事,阅读时从外到内扫资源,再看横切,再看业务。比把重试、计时、关连接全写进一个 80 行函数清晰得多。第 28 篇实战最小 Agent 时,你会把这些积木再拼一次。
七、踩坑大全
通用
坑 1:手写 open 却忘记 close。 现象:句柄泄漏、缓冲未刷盘。解决:一律 with open(...);自定义资源实现协议或用 closing。
坑 2:exit 返回 True 误吞异常。 现象:错误消失,后续逻辑「假成功」。解决:默认 return False;仅对明确类型抑制,并打日志。
坑 3:@contextmanager 忘记 try/finally。 现象:块体异常时清理代码被跳过。解决:yield 必须包在 try/finally 里。
坑 4:同一不可重入锁嵌套 with。 现象:死锁。解决:用 RLock,或避免嵌套,或拆函数降低持锁范围。
坑 5:在 enter 里获取了多个资源,中途失败未回滚。 现象:部分资源泄漏。解决:用 ExitStack 逐步 enter_context,或按获取逆序手写回滚。
坑 6:把上下文实例存成全局再反复 with,状态错乱。 现象:第二次进入看到脏状态。解决:每次 with Foo() 新建;或实现清晰的可重入栈。
macOS / Linux
坑 7:chdir 影响整个进程。 现象:多线程里改 cwd 彼此踩脚。解决:尽量用绝对路径 / Path,避免依赖 cwd;必须改时缩短窗口并加锁(仍难彻底安全)。
坑 8:临时目录在异常后仍想保留调试。 现象:TemporaryDirectory 默认删除。解决:调试期改用固定路径,或 delete=False(注意版本差异)并人工清理。
Windows
坑 9:文件仍被占用导致删临时目录失败。 现象:PermissionError。解决:确保 with 块内打开的文件已退出嵌套;避免把目录句柄留到块外。
与装饰器叠加
坑 10:在生成器函数上既 @contextmanager 又叠业务装饰器,顺序混乱。 现象:yield 语义被吃掉。解决:@contextmanager 紧贴 def;其它装饰器想清楚是包「工厂」还是包「上下文对象」。参考第 16 篇栈顺序。
坑 11:在 exit 里抛出新异常,掩盖原异常。 现象:排障时只看见清理错误,根因丢失。解决:清理代码尽量宽宥并记录;必须抛时考虑异常链(raise ... from),Python 3 在 exit 再抛时通常会体现上下文,但仍应避免粗暴覆盖。
坑 12:把耗时很长的业务放在持锁的 with 里。 现象:吞吐崩、超时连锁。解决:缩小临界区——只保护共享状态读写,网络 IO 放锁外(第 23、25 篇会再强调)。
八、小结
合上文章后,试着用自己的话说明:with 如何变成 enter / exit,以及 @contextmanager 的 yield 前后各扮演什么角色。讲得清,才算真的会了。动手至少跑通:类式 Timer、生成器版 timer、ExitStack 打开多文件、以及一个「假 session」上下文。
- 协议:
with=enter→ 块体 →exit(exc_type, exc_val, exc_tb);退出必定发生。 - 返回值:
enter的返回值绑定到as;exit返回真则抑制异常。 - 两种写法:类适合有状态 / 可重入;
@contextmanager适合前后钩子,yield只一次且用finally清理。 - 组合:固定层数用多层
with;动态个数用ExitStack;退出顺序后进先出。 - 标准库:
nullcontext、suppress、TemporaryDirectory、锁与官方chdir是日常刀具。 - AI Infra:
no_grad、会话、锁、临时目录都是块级边界;与第 16 篇装饰器常组合使用。
把故事连起来:第 10 篇 with open → 第 11 篇 try/finally → 第 14 篇点到协议 → 第 15 篇生成器 → 第 16 篇装饰器 → 本篇把块级资源协议立成一等工具。专栏二的工程刀具继续成套:流式(15)→ 横切(16)→ 资源(17)→ 类型(18)。
若只能带走一句话:上下文管理器就是「可进入 / 可退出」;with 保证用完必清理。
也可以用「三个不要」自检:不要在 exit 里随手 return True;不要在 @contextmanager 里省掉 finally;不要用不可重入锁嵌套。
再留一个自测:画出 with A() as a, B() as b 的进入/退出顺序;说出 exit 返回真的后果;把 @timer 改写成 with Timer()——三问都顺,本篇就站稳了。
本篇技能会立刻复利:读训练脚本先找 with;写 Agent 时用会话上下文包住多轮调用;写测试时用临时目录与 nullcontext。后续类型注解、测试、异步,都是在「有清晰边界的块与函数」上继续加约束。
和专栏一串起来复习也不亏:文件与 with(10)是样板;异常(11)解释抑制语义;数据模型(14)提供插头名;迭代器(15)支撑生成器写法;装饰器(16)提供函数级对照——本篇是专栏二把「资源生命周期」立成协议的一课。
也可以把收获压成检查清单:能翻译 with;能手写类式与生成器式;能解释异常三参数;能用 ExitStack;能举一个 AI Infra 中的 with 例子。五项过关,即可进入类型注解篇。
若把本篇压成给同事的三十秒讲解:先画「enter / body / exit」;再画「yield 前后」;最后强调「默认不吞异常、退出后进先出」。三张草图比贴一篇博客更管用。等对方能独立写出 @contextmanager 版计时器并解释 exit 返回值,就可以一起 code review 你们项目里的会话与锁了。
九、课后练习
- (基础) 实现类式
Timer,在exit打印耗时;验证块内抛异常时仍会打印。 - (基础) 用
@contextmanager实现同一个timer;对比两种写法的代码量。 - (基础) 写
chdir(path)上下文,进入时切换、退出时还原;嵌套两个不同目录验证栈式还原(提示:每次新建实例,或内部用列表栈)。 - (理解) 让
exit在遇到ValueError时返回True,其它异常返回False;分别触发两种异常并观察。 - (理解) 用
ExitStack打开三个临时文本文件,读出合并字符串;故意让第二个路径无效,确认第一个已被关闭(可用包装类打印close)。 - (理解) 用
nullcontext写函数run(use_lock: bool):为真时with lock,为假时with nullcontext(),业务代码只写一份。 - (进阶) 实现可重入的
TempEnv(key, value):嵌套设置同一环境变量时正确压栈还原(os.environ)。 - (进阶 · AI Infra) 写
fake_session与带@retry的call_llm(可用第 16 篇思路),组合为「外层 with 会话、内层重试调用」;讨论:计时上下文应包整段循环还是单次调用,才能表示「用户可感知延迟」?
下一篇预告
下一篇是《第18篇|类型注解与 dataclass、Pydantic》——给函数与数据结构加上可检查的合同:typing 基础、Optional / list[str]、Protocol、@dataclass、以及用 Pydantic 做运行时校验。你会看到:装饰器与上下文管「行为边界」,类型注解管「数据形状」;三者一起,才是工程化 Python 的标配。
专栏二后续路线:类型注解 / dataclass / Pydantic → 标准库精选(collections / itertools / functools)→ 虚拟环境与打包 → 日志调试性能 → pytest → 并发与 asyncio → HTTP / FastAPI → 最小 Agent。带着本篇的资源边界直觉,读源码时先找 with 与 @,再跟类型签名。
若想提前热身:给本篇的 Timer.enter / exit 加上简单类型注解;试写一个 @dataclass 保存「请求配置」,再想想哪些字段适合在 Pydantic 里做范围校验。做完后第 18 篇谈静态与运行时检查时你会带着具体对比感。
也可以把第 16 篇的 @tool 注册表与上下文对照:装饰器收集元数据,上下文管理会话;类型注解则描述工具参数与返回值——Agent 三件套的轮廓已经清晰。
专栏一用十四篇把「对象与协议」立住;专栏二从迭代器(15)、装饰器(16)走到上下文(17),下一站是类型系统。下一篇见。
若时间充裕,再多做一件事:打开你写过的脚本,找出三处手写 try/finally 关资源或还原状态,改成 with。改完后你会更笃定:专栏二不是要你背更多语法,而是把已经会的异常与生成器,接到真实工程的资源边界上。
进入专栏二至此,三把刀具已经齐:生成器管「流」、装饰器管「横切」、上下文管「资源」。下一篇开始给数据和函数签名加类型合同,工程味道会更浓。若某一节读着抽象,回到对应插图把箭头走一遍,再手打示例——多数卡住的地方,其实是「退出顺序」或「异常该不该抑制」没在纸上画清楚。
收官前对照练习:同一份「调用 LLM」逻辑,分别用手工 try/finally、@retry+@timer、以及 with session + with Timer() 走一遍。你会清楚感受到:上下文管理器不是语法糖游戏,而是把资源生命周期抬到与框架会话 / 锁 / no_grad 同一套操作面的关键一步——也是专栏二工程化与框架对接的入场券之一。
