带有 `yield` 的依赖¶
FastAPI 支持在完成后执行一些 额外步骤 的依赖。
为此,请使用 `yield` 而非 `return`,并在其后编写额外的步骤(代码)。
提示
请确保每个依赖只使用一次 `yield`。
技术细节
任何有效函数,只要可以用于
都可以用作 FastAPI 依赖。
事实上,FastAPI 内部使用了这两个装饰器。
带有 `yield` 的数据库依赖¶
例如,你可以用它来创建数据库会话并在完成后关闭它。
只有 `yield` 语句之前和包含 `yield` 语句的代码会在创建响应之前执行
async def get_db():
db = DBSession()
try:
yield db
finally:
db.close()
`yield` 的值会被注入到 *路径操作* 和其他依赖中
async def get_db():
db = DBSession()
try:
yield db
finally:
db.close()
`yield` 语句后面的代码会在创建响应后但在发送响应前执行
async def get_db():
db = DBSession()
try:
yield db
finally:
db.close()
提示
你可以使用 `async` 或普通函数。
FastAPI 将对它们进行正确处理,与处理普通依赖的方式相同。
带有 `yield` 和 `try` 的依赖¶
如果在带有 `yield` 的依赖中使用 `try` 块,你将收到在使用该依赖时抛出的任何异常。
例如,如果在中间某个点,在另一个依赖或 *路径操作* 中,某个代码导致数据库事务“回滚”或产生任何其他错误,你将在你的依赖中收到该异常。
因此,你可以在依赖内部使用 `except SomeException` 来查找该特定异常。
同样,你可以使用 `finally` 来确保退出步骤被执行,无论是否发生异常。
async def get_db():
db = DBSession()
try:
yield db
finally:
db.close()
带有 `yield` 的子依赖¶
你可以拥有任意大小和形状的子依赖和子依赖“树”,其中任何或所有都可以使用 `yield`。
FastAPI 将确保带有 `yield` 的每个依赖中的“退出代码”以正确的顺序运行。
例如,`dependency_c` 可以依赖于 `dependency_b`,而 `dependency_b` 依赖于 `dependency_a`
from typing import Annotated
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
🤓 其他版本和变体
from fastapi import Depends
from typing_extensions import Annotated
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
提示
如果可能,请优先使用 Annotated
版本。
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a=Depends(dependency_a)):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b=Depends(dependency_b)):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
并且它们都可以使用 `yield`。
在这种情况下,`dependency_c` 要执行其退出代码,需要 `dependency_b`(这里命名为 `dep_b`)的值仍然可用。
反过来,`dependency_b` 需要 `dependency_a`(这里命名为 `dep_a`)的值对其退出代码可用。
from typing import Annotated
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
🤓 其他版本和变体
from fastapi import Depends
from typing_extensions import Annotated
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
提示
如果可能,请优先使用 Annotated
版本。
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a=Depends(dependency_a)):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b=Depends(dependency_b)):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
同样,你可以拥有一些带有 `yield` 的依赖和一些带有 `return` 的依赖,并且让其中一些依赖于另一些。
你还可以有一个依赖需要多个其他带有 `yield` 的依赖,等等。
你可以拥有任何你想要的依赖组合。
FastAPI 将确保一切都以正确的顺序运行。
带有 `yield` 和 `HTTPException` 的依赖¶
你已经看到可以使用带有 `yield` 的依赖,并拥有捕获异常的 `try` 块。
同样,你可以在退出代码中,`yield` 之后抛出 `HTTPException` 或类似异常。
提示
这是一种比较高级的技术,在大多数情况下你并不真的需要它,因为你可以从应用程序代码的其他部分(例如,在 *路径操作函数* 中)抛出异常(包括 `HTTPException`)。
但如果你需要,它就在那里。🤓
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
data = {
"plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"},
"portal-gun": {"description": "Gun to create portals", "owner": "Rick"},
}
class OwnerError(Exception):
pass
def get_username():
try:
yield "Rick"
except OwnerError as e:
raise HTTPException(status_code=400, detail=f"Owner error: {e}")
@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
if item_id not in data:
raise HTTPException(status_code=404, detail="Item not found")
item = data[item_id]
if item["owner"] != username:
raise OwnerError(username)
return item
🤓 其他版本和变体
from fastapi import Depends, FastAPI, HTTPException
from typing_extensions import Annotated
app = FastAPI()
data = {
"plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"},
"portal-gun": {"description": "Gun to create portals", "owner": "Rick"},
}
class OwnerError(Exception):
pass
def get_username():
try:
yield "Rick"
except OwnerError as e:
raise HTTPException(status_code=400, detail=f"Owner error: {e}")
@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
if item_id not in data:
raise HTTPException(status_code=404, detail="Item not found")
item = data[item_id]
if item["owner"] != username:
raise OwnerError(username)
return item
提示
如果可能,请优先使用 Annotated
版本。
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
data = {
"plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"},
"portal-gun": {"description": "Gun to create portals", "owner": "Rick"},
}
class OwnerError(Exception):
pass
def get_username():
try:
yield "Rick"
except OwnerError as e:
raise HTTPException(status_code=400, detail=f"Owner error: {e}")
@app.get("/items/{item_id}")
def get_item(item_id: str, username: str = Depends(get_username)):
if item_id not in data:
raise HTTPException(status_code=404, detail="Item not found")
item = data[item_id]
if item["owner"] != username:
raise OwnerError(username)
return item
另一种捕获异常(并可能也抛出另一个 `HTTPException`)的方法是创建 自定义异常处理器。
带有 `yield` 和 `except` 的依赖¶
如果你在带有 `yield` 的依赖中使用 `except` 捕获异常,并且没有再次抛出它(或抛出新异常),FastAPI 将无法感知到异常的发生,这与普通 Python 的行为相同。
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
class InternalError(Exception):
pass
def get_username():
try:
yield "Rick"
except InternalError:
print("Oops, we didn't raise again, Britney 😱")
@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
if item_id == "portal-gun":
raise InternalError(
f"The portal gun is too dangerous to be owned by {username}"
)
if item_id != "plumbus":
raise HTTPException(
status_code=404, detail="Item not found, there's only a plumbus here"
)
return item_id
🤓 其他版本和变体
from fastapi import Depends, FastAPI, HTTPException
from typing_extensions import Annotated
app = FastAPI()
class InternalError(Exception):
pass
def get_username():
try:
yield "Rick"
except InternalError:
print("Oops, we didn't raise again, Britney 😱")
@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
if item_id == "portal-gun":
raise InternalError(
f"The portal gun is too dangerous to be owned by {username}"
)
if item_id != "plumbus":
raise HTTPException(
status_code=404, detail="Item not found, there's only a plumbus here"
)
return item_id
提示
如果可能,请优先使用 Annotated
版本。
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
class InternalError(Exception):
pass
def get_username():
try:
yield "Rick"
except InternalError:
print("Oops, we didn't raise again, Britney 😱")
@app.get("/items/{item_id}")
def get_item(item_id: str, username: str = Depends(get_username)):
if item_id == "portal-gun":
raise InternalError(
f"The portal gun is too dangerous to be owned by {username}"
)
if item_id != "plumbus":
raise HTTPException(
status_code=404, detail="Item not found, there's only a plumbus here"
)
return item_id
在这种情况下,客户端将按预期收到 *HTTP 500 内部服务器错误* 响应,因为我们没有抛出 `HTTPException` 或类似异常,但服务器将 没有任何日志 或其他指示错误的信息。😱
总是在带有 `yield` 和 `except` 的依赖中 `raise` 异常¶
如果你在带有 `yield` 的依赖中捕获了异常,除非你抛出另一个 `HTTPException` 或类似异常,否则你应该重新抛出原始异常。
你可以使用 `raise` 重新抛出相同的异常
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
class InternalError(Exception):
pass
def get_username():
try:
yield "Rick"
except InternalError:
print("We don't swallow the internal error here, we raise again 😎")
raise
@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
if item_id == "portal-gun":
raise InternalError(
f"The portal gun is too dangerous to be owned by {username}"
)
if item_id != "plumbus":
raise HTTPException(
status_code=404, detail="Item not found, there's only a plumbus here"
)
return item_id
🤓 其他版本和变体
from fastapi import Depends, FastAPI, HTTPException
from typing_extensions import Annotated
app = FastAPI()
class InternalError(Exception):
pass
def get_username():
try:
yield "Rick"
except InternalError:
print("We don't swallow the internal error here, we raise again 😎")
raise
@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
if item_id == "portal-gun":
raise InternalError(
f"The portal gun is too dangerous to be owned by {username}"
)
if item_id != "plumbus":
raise HTTPException(
status_code=404, detail="Item not found, there's only a plumbus here"
)
return item_id
提示
如果可能,请优先使用 Annotated
版本。
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
class InternalError(Exception):
pass
def get_username():
try:
yield "Rick"
except InternalError:
print("We don't swallow the internal error here, we raise again 😎")
raise
@app.get("/items/{item_id}")
def get_item(item_id: str, username: str = Depends(get_username)):
if item_id == "portal-gun":
raise InternalError(
f"The portal gun is too dangerous to be owned by {username}"
)
if item_id != "plumbus":
raise HTTPException(
status_code=404, detail="Item not found, there's only a plumbus here"
)
return item_id
现在客户端将收到相同的 *HTTP 500 内部服务器错误* 响应,但服务器日志中将包含我们的自定义 `InternalError`。😎
带有 `yield` 的依赖的执行¶
执行序列大致如下图所示。时间从上到下流动。每列代表一个交互或执行代码的部分。
sequenceDiagram
participant client as Client
participant handler as Exception handler
participant dep as Dep with yield
participant operation as Path Operation
participant tasks as Background tasks
Note over client,operation: Can raise exceptions, including HTTPException
client ->> dep: Start request
Note over dep: Run code up to yield
opt raise Exception
dep -->> handler: Raise Exception
handler -->> client: HTTP error response
end
dep ->> operation: Run dependency, e.g. DB session
opt raise
operation -->> dep: Raise Exception (e.g. HTTPException)
opt handle
dep -->> dep: Can catch exception, raise a new HTTPException, raise other exception
end
handler -->> client: HTTP error response
end
operation ->> client: Return response to client
Note over client,operation: Response is already sent, can't change it anymore
opt Tasks
operation -->> tasks: Send background tasks
end
opt Raise other exception
tasks -->> tasks: Handle exceptions in the background task code
end
信息
只会向客户端发送 一个响应。它可能是错误响应之一,也可能是来自 *路径操作* 的响应。
一旦发送了其中一个响应,就不能再发送其他响应。
提示
此图显示了 `HTTPException`,但你也可以抛出在带有 `yield` 的依赖中或通过 自定义异常处理器 捕获的任何其他异常。
如果你抛出任何异常,它都将被传递给带有 yield 的依赖,包括 `HTTPException`。在大多数情况下,你会希望重新抛出相同的异常或从带有 `yield` 的依赖中抛出新异常,以确保其得到正确处理。
带有 `yield`、`HTTPException`、`except` 和后台任务的依赖¶
警告
你很可能不需要这些技术细节,可以跳过本节并继续阅读下文。
这些细节主要在你使用 0.106.0 之前的 FastAPI 版本并在后台任务中使用了带有 `yield` 的依赖中的资源时有用。
带有 `yield` 和 `except` 的依赖,技术细节¶
在 FastAPI 0.110.0 之前,如果你使用带有 `yield` 的依赖,然后在该依赖中使用 `except` 捕获异常,并且没有再次抛出异常,那么该异常将自动抛出/转发给任何异常处理器或内部服务器错误处理器。
此行为在 0.110.0 版本中已更改,以修复由于没有处理器的转发异常(内部服务器错误)导致的未处理内存消耗,并使其与普通 Python 代码的行为保持一致。
后台任务和带有 `yield` 的依赖,技术细节¶
在 FastAPI 0.106.0 之前,在 `yield` 之后抛出异常是不可能的,因为带有 `yield` 的依赖中的退出代码是在响应发送 *之后* 执行的,所以 异常处理器 已经运行。
这样设计主要是为了允许在后台任务内部使用依赖“yield”的相同对象,因为退出代码会在后台任务完成后执行。
然而,由于这意味着在响应通过网络传输时,不必要地持有带有 `yield` 的依赖中的资源(例如数据库连接),因此在 FastAPI 0.106.0 中更改了此行为。
提示
此外,后台任务通常是一组独立的逻辑,应该单独处理,拥有自己的资源(例如,自己的数据库连接)。
因此,这样你可能会有更简洁的代码。
如果你曾经依赖于此行为,现在你应该在后台任务本身内部创建后台任务所需的资源,并且内部只使用不依赖于带有 `yield` 的依赖的资源的数据。
例如,你不会使用相同的数据库会话,而是在后台任务内部创建一个新的数据库会话,然后使用这个新会话从数据库获取对象。然后,不是将数据库中的对象作为参数传递给后台任务函数,而是传递该对象的 ID,然后在后台任务函数内部再次获取该对象。
上下文管理器¶
什么是“上下文管理器”¶
“上下文管理器”是任何可以在 `with` 语句中使用的 Python 对象。
例如,你可以 使用 `with` 读取文件
with open("./somefile.txt") as f:
contents = f.read()
print(contents)
在底层,`open("./somefile.txt")` 创建了一个被称为“上下文管理器”的对象。
当 `with` 块结束时,它会确保关闭文件,即使发生了异常。
当你创建一个带有 `yield` 的依赖时,FastAPI 将在内部为其创建一个上下文管理器,并将其与一些其他相关工具结合使用。
在带有 `yield` 的依赖中使用上下文管理器¶
警告
这或多或少是一个“高级”概念。
如果你刚开始使用 FastAPI,你可能现在想跳过它。
在 Python 中,你可以通过 创建一个包含两个方法:`__enter__()` 和 `__exit__()` 的类 来创建上下文管理器。
你也可以在 FastAPI 带有 `yield` 的依赖中,通过在依赖函数内部使用 `with` 或 `async with` 语句来使用它们。
class MySuperContextManager:
def __init__(self):
self.db = DBSession()
def __enter__(self):
return self.db
def __exit__(self, exc_type, exc_value, traceback):
self.db.close()
async def get_db():
with MySuperContextManager() as db:
yield db
提示
创建上下文管理器的另一种方法是使用
用单个 `yield` 装饰函数。
这就是 FastAPI 内部用于带有 `yield` 的依赖的方式。
但是你不必为 FastAPI 依赖使用装饰器(也不应该使用)。
FastAPI 会在内部为你完成。