Python 上下文管理器
with 语句配对资源获取与释放;实现 __enter__/__exit__ 或使用 contextlib。
#concept
#growing
#dev
#python
#python
[!info] related notes
Python 上下文管理器
这个概念为什么出现
文件、锁、事务、临时目录必须“借出后归还”。只靠程序员记得 close() 会在异常路径泄漏。with 把获取/释放写成结构。
[!abstract] 一句话理解
__enter__成功后,with 在块正常结束、return/break 或异常退出时调用__exit__。是否释放资源或抑制异常取决于实现;进程强制终止等情况不能靠它保证清理。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:模拟库存锁的进入与退出协议
本例的 held 只是布尔标志,用于观察协议顺序,不提供线程、进程或数据库互斥;真正的库存并发控制需用共享锁或事务。
# reserve_lock_cm.py
# 业务意图:预留库存时持有锁,结束或失败都释放。
# 教学点:
# - __enter__/__exit__;
# - 异常仍触发 exit;
# - exit 返回 False 传播异常。
class InventoryLock:
def __init__(self, sku: str) -> None:
self.sku = sku
self.held = False
def __enter__(self) -> "InventoryLock":
self.held = True
print(f"lock acquire {self.sku}")
return self
def __exit__(self, exc_type, exc, tb) -> bool:
self.held = False
print(f"lock release {self.sku}; exc={exc_type.__name__ if exc_type else None}")
return False # 不吞异常
def reserve(sku: str, ok: bool) -> None:
with InventoryLock(sku) as lock:
print("held?", lock.held)
if not ok:
raise RuntimeError("upstream fail")
print("reserved")
def main() -> None:
reserve("SKU-1", True)
try:
reserve("SKU-2", False)
except RuntimeError as exc:
print("caught:", exc)
if __name__ == "__main__":
main()
建议运行:
python reserve_lock_cm.py
期望输出:
lock acquire SKU-1
held? True
reserved
lock release SKU-1; exc=None
lock acquire SKU-2
held? True
lock release SKU-2; exc=RuntimeError
caught: upstream fail
结合场景再看三个关注点
- 失败路径也释放锁——
__exit__仍执行。 - 返回 False 让异常继续传播给调用方。
- 真实项目可用
threading.Lock/contextlib。
核心概念与准确模型
with cm as x:
...
# enter 成功后进入 body;退出时把异常信息交给 exit,exit 可决定抑制异常。
- contextlib.contextmanager 要求生成器恰好 yield 一次;获取放 yield 前,释放放 try/finally,块内异常会在 yield 位置重新抛入。捕获后未重新抛出可能抑制该异常。
- 多上下文从左到右进入、反向退出;B 进入失败时,已经成功进入的 A 仍会退出。
- 异步:
async with+__aenter__/__aexit__
边界情况与反直觉行为
__exit__返回真值会抑制块内异常;返回假值/None 则传播。若清理自身抛出新异常,传播的异常会改变,原异常通常保留为上下文。__enter__失败时不会调用该管理器的__exit__,进入过程已取得的资源需自行清理。- 可复用与可重入不同:顺序再次使用不代表可以嵌套进入同一个对象;contextmanager 产生的单个管理器通常只能进入一次。
常见误区
[!warning] 常见误区:with 只是语法糖可有可无 错误理解:finally 手写一样。
正确模型:协议化清理减少遗漏,是资源安全默认写法。
工程实践
- 文件:
with open(...) - 自研资源优先 CM
- 测试断言异常后资源状态
本节总结
上下文管理器把生命周期变成语法结构。有获取就有释放,异常不是借口。
自测题
- 块内异常时
__exit__会执行吗? - 何时用
contextlib.contextmanager?
参考答案
- 正常 Python 控制流中,只要 enter 成功就会调用 exit;仍需实现正确清理,不能覆盖强制终止进程等情况。
- 逻辑简单、想用生成器 yield 前后写获取/释放时。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| with statement | 规范 | 语法 |
| contextlib | 文档 | 工具 |