这一篇我们会学习另一个在后端中非常重要的能力:
with
__enter__
__exit__
contextmanager
yield
async with
它们会解释为什么文件、数据库连接、HTTP 客户端、模型资源都需要被正确打开和关闭。
一、前言
在编程中,有很多资源不能只打开、不关闭。例如:
文件 数据库连接 网络连接 HTTP 客户端 Redis 客户端 消息队列连接 模型文件 线程锁如果资源没有被正确释放,轻则出现文件无法删除、数据库连接耗尽,重则服务运行一段时间后崩溃。在这里Python 提供了非常优雅的资源管理方式:使用with...你在后端项目中已经见过类似思想:
def get_db(): db = SessionLocal() try: yield db finally: db.close()这一篇就从最简单的文件操作开始,逐步理解:
什么是资源管理?
为什么不能只 open 不 close?
with 到底做了什么?
__enter__ 和 __exit__ 是什么?
contextmanager 和 yield 如何工作?
async with 和普通 with 有什么区别?
FastAPI 的 get_db 和 lifespan 为什么与它有关?
二、什么是资源?为什么需要释放?
资源可以理解成程序向操作系统、数据库或外部服务“借来的东西”。例如打开一个文件:
file = open("notes.txt", "r", encoding="utf-8")程序得到一个文件对象,并获得读取文件的能力。当读取结束后,需要:
file.close()告诉系统:文件已经用完,可以释放相关资源了。如果你只打开而不关闭可能出现:
文件句柄不断累积
Windows 下文件可能被占用
无法重命名、移动或删除文件
程序长期运行后资源耗尽
数据库连接同理:
后端请求到来 ↓ 创建数据库 Session ↓ 查询或修改数据 ↓ 请求结束 ↓ 必须关闭 Session如果每个请求都连接数据库,却从不关闭,连接会越来越多,最终数据库拒绝新的请求。
三、最原始的文件操作方式
假设有一个文件:
notes.txt内容是:学习 Python
学习 FastAPI
学习 SQLAlchemy
最基础的读取方式:
file = open("notes.txt", "r", encoding="utf-8") content = file.read() file.close() print(content)这里的open()表示打开文件,"note.txt"是文件的路径,"r"是表示读取模式,read。encoding="utf-8"表示按 UTF-8 编码读取中文文本。file.read()读取文件全部内容。file.close()表示关闭文件。
这种写法可以运行,但有一个问题:如果读取过程出错,close()可能来不及执行。
例如:
file = open("notes.txt", "r", encoding="utf-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", encoding="utf-8") try: content = file.read() print(content) finally: file.close()finally的含义是:
无论
try中成功、失败、抛异常,都会执行。
四、with:Python 推荐的资源管理写法
对于文件或者数据库的连接关闭其实过于繁琐,所以python中提供了更加简洁的方式:
with open("notes.txt", "r", encoding="utf-8") as file: content = file.read() print(content)这段代码等价于“打开文件、使用文件、最后自动关闭文件”。其中:with open(...) as file:可以理解为:
打开一个资源,并把它暂时赋值给变量
file;当缩进代码块结束时,自动执行清理工作。
不管读取成功,还是中间报错,文件最终都会被关闭。因此文件操作推荐优先写成:
with open("notes.txt", "r", encoding="utf-8") as file: content = file.read()4.1as file是什么语法?
右侧的open("notes.txt", "r", encoding="utf-8")表示创建文件对象。as file把这个文件对象赋值给变量file。所以缩进内部可以使用:
file.read() file.readline() file.write(...)例如逐行读取:
with open("notes.txt", "r", encoding="utf-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, mode="r"): file = open(path, mode, encoding="utf-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, mode="r"): file = open(path, mode, encoding="utf-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、rollback
close()的作用是关闭 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( username="kankan", password_hash="example_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( f"https://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就是异步版本的@contextmanager,lifespan是 FastAPI 专门接收这个异步上下文管理器的参数,用来管理整个 Web 服务从启动到关闭的完整生命周期,靠yield切成两段:yield前面:服务启动阶段,在接收任何接口请求之前执行,做初始化;yield后面:服务关闭阶段,服务不再接收新请求之后执行,做资源清理。
lifespan:包裹整个 Web 服务,服务运行期间,就卡在 yield 这里。服务停止,才执行 yield 后的清理。
你当前项目中的启动逻辑是:
from contextlib import asynccontextmanager @asynccontextmanager async def lifespan(app: FastAPI): os.makedirs("data", exist_ok=True) Base.metadata.create_all(bind=engine) yield再传给 FastAPI:
app = FastAPI( title="FastAPI 注册登录 JWT 实战", lifespan=lifespan, )它和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", encoding="utf-8") as file: content = file.read() chunks = split_text(content) with session_scope() as db: document = Document( filename=file_path, status="processing", ) 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", encoding="utf-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 上下文管理、生成器和异常处理共同形成的工程能力。