Python 高阶语法(二):上下文管理器、with、yield 与资源安全释放——从文件到 FastAPI 数据库会话
这一篇我们会学习另一个在后端中非常重要的能力with__enter____exit__contextmanageryieldasync with它们会解释为什么文件、数据库连接、HTTP 客户端、模型资源都需要被正确打开和关闭。一、前言在编程中有很多资源不能只打开、不关闭。例如文件 数据库连接 网络连接 HTTP 客户端 Redis 客户端 消息队列连接 模型文件 线程锁如果资源没有被正确释放轻则出现文件无法删除、数据库连接耗尽重则服务运行一段时间后崩溃。在这里Python 提供了非常优雅的资源管理方式使用with...你在后端项目中已经见过类似思想def get_db(): db SessionLocal() try: yield db finally: db.close()这一篇就从最简单的文件操作开始逐步理解什么是资源管理为什么不能只 open 不 closewith 到底做了什么__enter__ 和 __exit__ 是什么contextmanager 和 yield 如何工作async with 和普通 with 有什么区别FastAPI 的 get_db 和 lifespan 为什么与它有关二、什么是资源为什么需要释放资源可以理解成程序向操作系统、数据库或外部服务“借来的东西”。例如打开一个文件file open(notes.txt, r, encodingutf-8)程序得到一个文件对象并获得读取文件的能力。当读取结束后需要file.close()告诉系统文件已经用完可以释放相关资源了。如果你只打开而不关闭可能出现文件句柄不断累积Windows 下文件可能被占用无法重命名、移动或删除文件程序长期运行后资源耗尽数据库连接同理后端请求到来 ↓ 创建数据库 Session ↓ 查询或修改数据 ↓ 请求结束 ↓ 必须关闭 Session如果每个请求都连接数据库却从不关闭连接会越来越多最终数据库拒绝新的请求。三、最原始的文件操作方式假设有一个文件notes.txt内容是学习 Python学习 FastAPI学习 SQLAlchemy最基础的读取方式file open(notes.txt, r, encodingutf-8) content file.read() file.close() print(content)这里的open()表示打开文件note.txt是文件的路径r是表示读取模式read。encodingutf-8表示按 UTF-8 编码读取中文文本。file.read()读取文件全部内容。file.close()表示关闭文件。这种写法可以运行但有一个问题如果读取过程出错close()可能来不及执行。例如file open(notes.txt, r, encodingutf-8) content file.read() result 1 / 0 file.close()程序执行到result 1 / 0时就报错并中断ZeroDivisionError: division by zero下面的file.close()不会执行。所以下面来讲解遇到这种情况我们应该如何规避。3.1 try / finally无论是否报错都释放资源为了保证资源一定关闭可以写file open(notes.txt, r, encodingutf-8) try: content file.read() print(content) finally: file.close()finally的含义是无论try中成功、失败、抛异常都会执行。四、withPython 推荐的资源管理写法对于文件或者数据库的连接关闭其实过于繁琐所以python中提供了更加简洁的方式with open(notes.txt, r, encodingutf-8) as file: content file.read() print(content)这段代码等价于“打开文件、使用文件、最后自动关闭文件”。其中with open(...) as file:可以理解为打开一个资源并把它暂时赋值给变量file当缩进代码块结束时自动执行清理工作。不管读取成功还是中间报错文件最终都会被关闭。因此文件操作推荐优先写成with open(notes.txt, r, encodingutf-8) as file: content file.read()4.1as file是什么语法右侧的open(notes.txt, r, encodingutf-8)表示创建文件对象。as file把这个文件对象赋值给变量file。所以缩进内部可以使用file.read() file.readline() file.write(...)例如逐行读取with open(notes.txt, r, encodingutf-8) as file: for line in file: print(line.strip())这里的line.strip()会去掉每行末尾的换行符和多余空格。输出学习 Python 学习 FastAPI 学习 SQLAlchemy五、上下文管理器是什么能配合with使用的对象叫做上下文管理器。例如open()返回的文件对象就是上下文管理器。一个上下文管理器需要支持两个特殊方法__enter__ __exit__它们分别对应进入 with 代码块之前退出 with 代码块之后执行逻辑可以理解为resource context_manager.__enter__() try: 使用 resource finally: context_manager.__exit__(...)因此with 某个上下文管理器 as resource: 使用 resource本质上是一种更安全、更清晰的try / finally写法。六、自定义上下文管理器统计代码执行时间我们自己实现一个上下文管理器。from time import perf_counter class Timer: def __enter__(self): self.start perf_counter() print(开始计时) return self def __exit__(self, exc_type, exc_value, traceback): end perf_counter() print(f执行耗时{end - self.start:.6f} 秒) return False使用with Timer(): total sum(range(1_000_000)) print(total)执行流程进入 with ↓ 执行 Timer.__enter__() ↓ 执行 with 缩进内部的代码 ↓ 退出 with ↓ 执行 Timer.__exit__()这里return self表示把当前Timer对象交给as后面的变量。with Timer() as timer: total sum(range(1_000_000))此时timer就是Timer对象。6.1__exit__的三个参数def __exit__(self, exc_type, exc_value, traceback):这三个参数与异常有关。如果with块正常结束exc_type None exc_value None traceback None如果with块发生异常例如with Timer(): result 1 / 0那么exc_type就表示异常类型例如ZeroDivisionError。而exc_value就表示异常对象traceback表示错误发生的位置追踪信息。大多数情况下return False表示不要吞掉异常仍然让错误继续抛出。这通常是推荐做法。如果写return True表示“我已经处理这个异常不再往外抛”。例如class IgnoreError: def __enter__(self): print(开始执行) def __exit__(self, exc_type, exc_value, traceback): if exc_type is not None: print(f捕获到异常{exc_value}) return True使用with IgnoreError(): result 1 / 0 print(程序仍然继续执行)程序不会因为除零错误停止。但真实项目中不要随意吞掉异常。否则程序表面正常问题却被隐藏排查会非常困难。七、contextlib.contextmanager用 yield 快速创建上下文管理器如果每次都手写__enter__ __exit__有时会比较冗长。Python 标准库提供了contextlib.contextmanager它可以通过生成器和yield更简洁地创建上下文管理器。例如from contextlib import contextmanager contextmanager def open_text_file(path, moder): file open(path, mode, encodingutf-8) try: yield file finally: file.close()使用with open_text_file(notes.txt) as file: content file.read() print(content)这个函数执行顺序特别重要contextmanager def open_text_file(...): file open(...) try: yield file finally: file.close()可以理解为进入 with ↓ 执行 yield 前打开文件 ↓ yield file把文件对象交给 as file ↓ 执行 with 内部代码 ↓ 退出 with ↓ 从 yield 后继续执行 ↓ finally 中关闭文件这和你项目中的get_db()已经非常接近了。八、用上下文管理器写文件下面是一个安全写入文本的例子from contextlib import contextmanager contextmanager def open_text_file(path, moder): file open(path, mode, encodingutf-8) try: yield file finally: file.close() with open_text_file(study_notes.txt, w) as file: file.write(今天学习了 Python 上下文管理器。\n) file.write(理解了 with、yield 和 finally 的关系。\n)其中w表示写入模式。需要注意w 模式会覆盖原文件内容 a 模式会在文件末尾追加内容 r 模式用于读取文件如果你想保留旧内容并新增笔记通常使用a。例如with open_text_file(study_notes.txt, a) as file: file.write(下一步学习 FastAPI 数据库连接管理。\n)九、数据库连接为什么特别适合上下文管理器在我们的 FastAPI 小项目中有def get_db(): db SessionLocal() try: yield db finally: db.close()它和刚刚的文件管理代码本质一样。文件版本file open(...) try: yield file finally: file.close()数据库版本db SessionLocal() try: yield db finally: db.close()其中SessionLocal()创建一份当前请求可用的数据库 Session。yield db把这个 Session 提供给 FastAPI 路由函数。db.close()在请求结束时关闭数据库会话。因此注册接口def register(user_in: UserRegister, db: DbSession): ...执行时会发生用户请求注册接口 ↓ FastAPI 调用 get_db() ↓ db SessionLocal() ↓ yield db把 db 注入 register() ↓ register() 查询、添加、提交用户数据 ↓ 请求结束 ↓ 执行 db.close()这就是为什么db能在注册函数中使用又不会永远占用数据库连接。9.1 为什么数据库操作还需要 commit、rollbackclose()的作用是关闭 Session但它不等于提交数据。例如db.add(user) db.commit()其中db.add(user)把对象加入当前数据库事务。db.commit()真正提交事务把数据写入数据库。如果执行过程中发生错误理想情况下还需要db.rollback()撤销当前未提交或失败的事务状态。更完整的数据库依赖可以写成def get_db(): db SessionLocal() try: yield db except Exception: db.rollback() raise finally: db.close()这里except Exception:表示捕获执行过程中的异常。db.rollback()表示回滚当前事务。raise表示继续把原始异常抛出去让 FastAPI 处理并记录。finally: db.close()无论成功或失败最终关闭 Session。实际项目中事务由哪一层负责提交需要统一规范。例如小项目中可以在路由函数中明确写db.add(task) db.commit()这样读代码时非常直观。复杂项目中也可能将事务管理统一封装在 Service 层或上下文管理器中。关键原则是一个业务操作的提交与回滚边界必须清楚不能混乱地到处 commit。9.2 数据库 Session 上下文管理器示例下面是一个独立的 Session 管理器示例from contextlib import contextmanager contextmanager def session_scope(): db SessionLocal() try: yield db db.commit() except Exception: db.rollback() raise finally: db.close()使用with session_scope() as db: user User( usernamekankan, password_hashexample_hash, ) db.add(user)正常情况下创建 Session ↓ yield db ↓ db.add(user) ↓ with 块结束 ↓ db.commit() ↓ db.close()如果中途报错创建 Session ↓ yield db ↓ 执行数据库操作时发生异常 ↓ db.rollback() ↓ db.close() ↓ 异常继续抛出这个模式非常适合脚本、后台任务或批量导入。但在 FastAPI 项目中如果多个地方都自动提交同时路由中又手动db.commit()就会造成事务边界不清晰。因此在 Web 项目中团队通常会统一一种规则例如规则一路由或 Service 显式 commit 规则二统一事务上下文自动 commit选择一种并坚持使用比混用更重要。十、async with异步资源管理前面使用的是with它适用于同步资源。如果资源的打开或关闭过程本身需要等待网络例如异步 HTTP 请求、异步数据库连接就需要async with例如import httpx async def get_weather(city): async with httpx.AsyncClient() as client: response await client.get( fhttps://example.com/weather?city{city} ) return response.json()这里async with httpx.AsyncClient() as client:表示创建异步 HTTP 客户端。await client.get(...)表示等待网络请求返回。当缩进代码块结束时HTTP 客户端会自动关闭连接。同步与异步的对比with 某资源 as resource: 使用同步资源async with 某资源 as resource: 使用异步资源如果资源需要await才能打开或关闭就不能使用普通with。十一、FastAPI 的 lifespan 与asynccontextmanager首先asynccontextmanager就是异步版本的contextmanagerlifespan是 FastAPI 专门接收这个异步上下文管理器的参数用来管理整个 Web 服务从启动到关闭的完整生命周期靠yield切成两段yield前面服务启动阶段在接收任何接口请求之前执行做初始化yield后面服务关闭阶段服务不再接收新请求之后执行做资源清理。lifespan包裹整个 Web 服务服务运行期间就卡在 yield 这里。服务停止才执行 yield 后的清理。你当前项目中的启动逻辑是from contextlib import asynccontextmanager asynccontextmanager async def lifespan(app: FastAPI): os.makedirs(data, exist_okTrue) Base.metadata.create_all(bindengine) yield再传给 FastAPIapp FastAPI( titleFastAPI 注册登录 JWT 实战, lifespanlifespan, )它和contextmanager的思路相同只是这里是异步版本。执行过程FastAPI 服务启动 ↓ 执行 yield 前的代码 ↓ 创建 data 文件夹 ↓ 创建 users、tasks 数据表 ↓ 执行到 yield服务开始接收请求 ↓ 服务关闭 ↓ 从 yield 后继续执行 ↓ 完成收尾工作如果未来需要在服务关闭时释放资源可以写asynccontextmanager async def lifespan(app: FastAPI): print(服务启动加载模型或建立连接) yield print(服务关闭释放模型或关闭连接)在 AI 应用中lifespan 常用于启动时加载 Embedding 模型 启动时创建向量数据库客户端 启动时读取配置 启动时建立模型服务连接 关闭时释放资源 关闭 HTTP 客户端 关闭模型连接 写入最终日志十二、真实 AI 项目中的上下文管理器案例假设未来你写一个“上传文档并构建知识库”的接口。处理过程可能包含接收 PDF ↓ 读取文件 ↓ 提取文本 ↓ 切分文档 ↓ 生成向量 ↓ 写入向量数据库 ↓ 保存文档状态其中至少有三类资源上传文件 数据库 Session HTTP / 模型客户端思路上可能是def process_document(file_path): with open(file_path, r, encodingutf-8) as file: content file.read() chunks split_text(content) with session_scope() as db: document Document( filenamefile_path, statusprocessing, ) db.add(document) return chunks如果模型调用是异步的async def create_embeddings(texts): async with httpx.AsyncClient() as client: response await client.post( https://example.com/embeddings, json{texts: texts}, ) return response.json()这就是上下文管理器在真实 AI 后端中的位置正确申请资源 ↓ 正确使用资源 ↓ 正确释放资源十三、常见错误总结13.1 打开文件后忘记关闭不推荐file open(notes.txt) content file.read()推荐with open(notes.txt, encodingutf-8) as file: content file.read()13.2 误以为close()等于commit()db.close()只负责关闭 Session。db.commit()才负责提交数据库事务。13.3 在__exit__中随意return True这会吞掉异常可能让错误悄悄消失。除非你非常明确要处理异常否则推荐return False或者不显式返回。13.4 将耗时 CPU 任务放进with就以为不会阻塞with的作用是资源管理不是并发工具。例如 PDF 切分、批量向量化特别耗时仍然应该考虑后台 Worker 任务队列 多进程13.5 不理解yield的暂停与恢复在上下文管理器或 FastAPI 依赖中yield resource不是彻底结束函数。它表示先把资源交出去 ↓ 等待外部代码执行 ↓ 再回来执行 yield 后的清理代码十九、本文总结这一篇最重要的链路是资源需要被正确释放 ↓ try / finally 可以保证清理 ↓ with 是更优雅的资源管理写法 ↓ __enter__ 在进入 with 前执行 __exit__ 在离开 with 后执行 ↓ contextmanager 可以用 yield 简化写法 ↓ asynccontextmanager 对应异步资源生命周期 ↓ FastAPI 的 get_db 和 lifespan 都应用了相同思想你现在再看项目中的代码def get_db(): db SessionLocal() try: yield db finally: db.close()应该能清楚理解创建数据库 Session ↓ 把 Session 交给接口使用 ↓ 请求结束后关闭 Session这不是 FastAPI 特有的魔法而是 Python 上下文管理、生成器和异常处理共同形成的工程能力。