【Python AI教程】(二)上下文管理器:资源管理的艺术
一行
with语句,胜过十行try...finally。上下文管理器是 Python 最被低估的特性之一,在 AI 开发中,它是管理 API 会话、LLM 调用、数据库连接的核心利器。
前言
写 Python AI 应用时,你是否曾为忘记关闭 API 会话而烦恼?是否为嵌套的 try...finally 块头疼?上下文管理器(Context Manager)就是解决这些问题的银弹。
本文将深入讲解:
__enter__/__exit__协议@contextmanager装饰器ExitStack动态资源管理- 嵌套上下文的高级用法
- AI 实战:API Session、LLM 响应处理、重试机制
一、上下文管理器是什么?
上下文管理器是一种 Python 协议,用于管理资源的获取和释放。核心思想:用 with 语句包裹一段代码,确保资源在使用完毕后一定被清理。
1.1 生命周期图解
graph TB
START["🔵 with 语句入口"] --> ENTER["📗 __enter__()<br/>获取/初始化资源"]
ENTER --> BODY["⚙️ 执行 with 块内代码"]
BODY --> EXIT["📕 __exit__()<br/>清理/释放资源"]
EXIT --> END["🏁 块外继续执行"]
BODY -.->|"发生异常"| EXC["📕 __exit__(exc_type,<br/>exc_val, exc_tb)"]
EXC -->|返回 True| SUPP["✅ 异常被 suppress"]
EXC -->|返回 False| RERAISE["❌ 异常向外传播"]
style START fill:#C7CEEA,stroke:#9FA8DA,color:#333
style ENTER fill:#B5EAD7,stroke:#80CBC4,color:#333
style BODY fill:#FFDAB9,stroke:#FFAB76,color:#333
style EXIT fill:#FFB3C6,stroke:#F48FB1,color:#333
style EXC fill:#FFF9C4,stroke:#F9A825,color:#333
style SUPP fill:#B5EAD7,stroke:#80CBC4,color:#333
style RERAISE fill:#FFB3C6,stroke:#F48FB1,color:#333
style END fill:#E8D5F5,stroke:#CE93D8,color:#333关键点:
__enter__返回的值绑定到as后的变量__exit__的三个参数接收异常信息(无异常时均为None)- 返回
True= 吞掉异常,返回False= 让异常继续传播
二、enter / exit 协议
最标准的实现方式:定义一个类,实现这两个方法。
1 | from contextlib import contextmanager, ExitStack |
输出:
1 | LLM call: 0.0501s |
实际 AI 场景:LLM Session 管理
1 | class LLMSession: |
输出:
1 | 🔑 LLM session started: gpt-4 |
为什么这样做?
- API key、连接池在
__enter__中初始化 - 请求计数、错误处理在
__exit__中汇总 - 无论代码正常还是抛异常,资源一定被释放
三、@contextmanager:更优雅的写法
类实现方式虽然清晰,但有些繁琐。contextlib.contextmanager 让我们用生成器实现上下文管理器。
1 | # === @contextmanager 版本 === |
输出:
1 | API call: 0.0302s |
原理揭秘:
yield之前的代码 →__enter__逻辑yield的值 →with as绑定的值yield之后的代码 →__exit__逻辑(在finally中执行)
AI 实战:重试上下文
1 | import random |
四、ExitStack:动态管理多个资源
想象这样的场景:需要根据条件动态决定打开哪些资源,或者要在循环中打开 N 个资源。传统 with 语句无法满足。
ExitStack 就是答案:它像是一个资源管理器栈,可以动态 push 任意数量的上下文管理器。
graph TB
START["🚀 ExitStack() 创建空栈"] --> PUSH["📥 enter_context()<br/>动态添加资源"]
PUSH --> LOOP{"还有资源<br/>要添加?"}
LOOP -->|"是"| PUSH
LOOP -->|"否"| EXEC["⚙️ 执行实际逻辑"]
EXEC --> CLEANUP["📕 自动调用所有<br/>__exit__ / finally"]
CLEANUP --> END["🏁 所有资源已释放"]
style START fill:#C7CEEA,stroke:#9FA8DA,color:#333
style PUSH fill:#B5EAD7,stroke:#80CBC4,color:#333
style LOOP fill:#FFF9C4,stroke:#F9A825,color:#333
style EXEC fill:#FFDAB9,stroke:#FFAB76,color:#333
style CLEANUP fill:#FFB3C6,stroke:#F48FB1,color:#333
style END fill:#E8D5F5,stroke:#CE93D8,color:#333实际案例:批量计时器
1 | # === ExitStack: 动态管理多个资源 === |
AI 场景:动态加载多个 API 密钥
1 | class APIClient: |
五、嵌套上下文
Python 允许在 with 块内再写 with,形成嵌套上下文。内层和外层各自独立,不会混淆。
1 | # === 嵌套上下文 === |
输出:
1 | 📦 Open primary |
注意执行顺序:后进先出(LIFO)——内层先 close,外层后 close。
实战:多级缓存 + 数据库事务
1 | class Cache: |
六、AI 实战:完整案例
案例 1:带超时的 LLM 调用
1 | import signal |
案例 2:Token 计数上下文
1 | class TokenCounter: |
七、常见错误与最佳实践
| 错误写法 | 正确写法 | 说明 |
|---|---|---|
__exit__ 返回 True 但不处理异常 | 返回 False 或显式处理 | 返回 True 会吞掉所有异常 |
在 __enter__ 中抛出异常 | 在构造函数中验证 | __enter__ 异常无法被 with 块捕获 |
yield 后忘记 finally | 始终用 try...finally 包裹 | 确保清理代码一定执行 |
嵌套 with 返回值冲突 | 用不同变量名绑定 | with A() as a, B() as b: |
八、总结
graph LR
CM["🔵 上下文管理器<br/>with 语句"]
PROTO["📗 __enter__/__exit__ 协议<br/>类实现"]
DECO["📗 @contextmanager<br/>生成器实现"]
STACK["📗 ExitStack<br/>动态资源栈"]
CM --> PROTO
CM --> DECO
CM --> STACK
PROTO -->|"AI 场景"| LLM["🤖 LLM Session<br/>API 连接池"]
DECO -->|"AI 场景"| RETRY["🔄 Retry Context<br/>重试机制"]
STACK -->|"AI 场景"| BATCH["📦 批量工具调用"]
style CM fill:#C7CEEA,stroke:#9FA8DA,color:#333
style PROTO fill:#B5EAD7,stroke:#80CBC4,color:#333
style DECO fill:#FFDAB9,stroke:#FFAB76,color:#333
style STACK fill:#FFB3C6,stroke:#F48FB1,color:#333
style LLM fill:#E8D5F5,stroke:#CE93D8,color:#333
style RETRY fill:#FFF9C4,stroke:#F9A825,color:#333
style BATCH fill:#E8D5F5,stroke:#CE93D8,color:#333记住三点:
- 资源获取用
__enter__,释放用__exit__ @contextmanager= 生成器 + 协议,更简洁- ExitStack = 动态资源管理,解决不确定数量的资源问题
下期预告:【Python AI教程】(三)装饰器:给函数穿上一层外衣——深入解析 Python 装饰器原理,以及在 AI 开发中的实战应用。
代码已通过 Python 3.11+ 验证。
📚 Python AI教程 系列导航
本文是《Python AI教程》系列第 2/14 篇。
| 方向 | 章节 |
|---|---|
| ◀ 上一篇 | (一)闭包与装饰器 |
| 下一篇 ▶ | (三)生成器与迭代器 |