news 2026/10/21 21:51:00

Python 高阶语法(二):上下文管理器、with、yield 与资源安全释放——从文件到 FastAPI 数据库会话

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python 高阶语法(二):上下文管理器、with、yield 与资源安全释放——从文件到 FastAPI 数据库会话

这一篇我们会学习另一个在后端中非常重要的能力:

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 上下文管理、生成器和异常处理共同形成的工程能力。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 8:44:14

MySQL中的常用SQL语句

SQL语句的分类DDL(Data Definition Languages)语句:数据定义语言,这些语句定义了不同的数据段、数据库、表、列、索引等数据库对象的定义。常用的语句关键字主要包括 create、drop、alter、rename、truncate。其中 create 用于创建数据库对象,drop 用于删…

作者头像 李华
网站建设 2026/10/18 17:29:08

【BFS 解决拓扑排序】课程表

文章目录题目解析算法原理建图入度数组代码实现题目链接:207. 课程表 题目解析 拓扑排序(Topological sorting)要解决的问题是 如何给一个有向无环图的所有节点排序。 有向无环图 (Directed Acyclic Graph, 缩写 DAG&#xff09…

作者头像 李华
网站建设 2026/10/21 21:50:47

PHP四大加密技术全解析+实战笔记免费领

在PHP语言里, 加密的操作方式主要有那四种常用的办法。除了刚才提到的这几个方法以外, 还有一种跟网址链接相关的加密处理以及解密的方式是另外一种存在的情况。我希望这段文字内容对于你们正在进行的软件开发工作能够提供一点实际的帮助作用和使用价值参考。 另外, 我把之后整…

作者头像 李华
网站建设 2026/10/18 22:12:26

气源含水对气液增压器的故障影响有多大?选台湾钰腾从根源规避风险

很多机加工车间都有一个常年被忽略的隐形隐患:气源里的水。尤其是南方梅雨季、北方冬季温差大的时段,空压机出来的压缩空气里混着大量冷凝水,哪怕装了普通的油水分离器,也很难完全过滤干净。这些混在气源里的水顺着管路钻进气液增…

作者头像 李华
网站建设 2026/10/18 3:03:52

OSI与TCP/IP分层模型实战:从理论到网络故障排查

计算机网络这门课,很多人是把它当“死知识”来背的:OSI七层是哪七层、TCP/IP有哪几个协议、三次握手发几个包……背得滚瓜烂熟,一旦真碰上网络故障,照样不知道从哪下手。我带过的不少新人都有这个问题。究其原因,是没有…

作者头像 李华