FastAPI教程——依赖

16 阅读6分钟

FastAPI 一个非常好的设计特性,是一种叫作依赖注入的技术。这个术语听起来技术味很重,也有点晦涩,但它是 FastAPI 的一个关键方面,而且在很多层面上都出人意料地有用。本章会介绍 FastAPI 的内置能力,以及如何编写你自己的依赖。

什么是依赖?

依赖是在某个时刻你需要的特定信息。获取这些信息的通常方式,是在你需要它的时候,编写代码把它取出来。

当你编写一个 Web 服务时,在某些时候,你可能需要做以下事情:

从 HTTP 请求中收集输入参数
验证输入
为某些端点检查用户认证和授权
从数据源中查找数据,通常是数据库
发出指标、日志或追踪信息

Web 框架会把 HTTP 请求字节转换为数据结构,而你会在 Web 层函数中,根据需要从这些结构里摘取你想要的东西。

依赖带来的问题

在你需要的时候,拿到你想要的东西,并且外部代码不需要知道你是如何拿到它的,这听起来相当合理。但事实证明,这会带来一些后果:

测试

你无法测试函数的不同变体,因为这些变体可能会用不同方式查找依赖。

隐藏依赖

隐藏细节意味着,当外部代码发生变化时,你的函数所需要的代码可能会坏掉。

代码重复

如果你的依赖是一个常见依赖,比如在数据库中查找用户,或者组合来自 HTTP 请求的多个值,你可能会在多个函数中重复这段查找代码。

OpenAPI 可见性

FastAPI 为你生成的自动测试页面,需要从依赖注入机制中获取信息。

依赖注入

“依赖注入”这个术语比听起来简单:把一个函数需要的任何特定信息传入这个函数。传统做法之一,是传入一个辅助函数,然后你调用这个辅助函数来获取特定数据。

FastAPI 依赖

FastAPI 更进一步:你可以把依赖定义为函数的参数,然后 FastAPI 会自动调用它们,并把它们返回的值传入函数。例如,一个 user_dep 依赖可以从 HTTP 参数中获取用户的用户名和密码,在数据库中查找它们,并返回一个令牌,之后你可以使用这个令牌来跟踪该用户。你的 Web 处理函数永远不会直接调用它;它会在函数调用时被处理。

你已经见过一些依赖,只是之前没有看到它们被称为依赖:HTTP 数据源,比如 PathQueryBodyHeader。这些是函数或 Python 类,会从 HTTP 请求的不同区域中挖出所请求的数据。它们隐藏了细节,比如有效性检查和数据格式。

为什么不自己写函数来做这些事呢?你当然可以,但你不会拥有这些:

数据有效性检查
格式转换
自动文档

在许多其他 Web 框架中,你会在自己的函数内部做这些检查。你会在第 7 章中看到这方面的例子,该章会比较 FastAPI 与 Flask、Django 等 Python Web 框架。但在 FastAPI 中,你可以像处理内置依赖那样,处理自己的依赖。

编写一个依赖

在 FastAPI 中,依赖是某种会被执行的东西,因此依赖对象需要属于 Callable 类型,其中包括函数和类——也就是那些你可以用括号和可选参数来调用的东西。

示例 6-1 展示了一个 user_dep() 依赖函数,它接收 namepassword 字符串参数,并且如果用户有效,就返回 True。对于第一个版本,我们先让这个函数对任何内容都返回 True

示例 6-1 一个依赖函数

from fastapi import FastAPI, Depends, Params

app = FastAPI()

# the dependency function:
def user_dep(name: str = Params, password: str = Params):
    return {"name": name, "valid": True}

# the path function / web endpoint:
@app.get("/user")
def get_user(user: dict = Depends(user_dep)) -> dict:
    return user

这里,user_dep() 是一个依赖函数。它的行为类似一个 FastAPI 路径函数,也就是它知道 Params 等东西,但它上方没有路径装饰器。它是一个辅助函数,而不是 Web 端点本身。

路径函数 get_user() 表示它期望一个名为 user 的参数变量,而这个变量会从依赖函数 user_dep() 中获取值。

注意

get_user() 的参数中,我们不能写 user = user_dep,因为 user_dep 是一个 Python 函数对象。我们也不能写 user = user_dep(),因为那会在定义 get_user() 时就调用 user_dep() 函数,而不是在使用 get_user() 时调用。所以我们需要额外的 FastAPI 辅助函数 Depends(),用它在真正需要的时候调用 user_dep()

你可以在路径函数的参数列表中拥有多个依赖。

依赖作用域

你可以定义依赖,让它覆盖单个路径函数、一组路径函数,或者整个 Web 应用程序。

单个路径

在你的路径函数中,包含一个类似这样的参数:

def pathfunc(name: depfunc = Depends(depfunc)):

或者只写成这样:

def pathfunc(name: depfunc = Depends()):

name 是你想给 depfunc 返回值取的任何名字。

来自前面的例子:

pathfuncget_user()
depfuncuser_dep()
nameuser

示例 6-2 使用这个路径和依赖,返回一个固定的用户名和一个表示有效性的布尔值。

示例 6-2 返回一个用户依赖

from fastapi import FastAPI, Depends, Params

app = FastAPI()

# the dependency function:
def user_dep(name: str = Params, password: str = Params):
    return {"name": name, "valid": True}

# the path function / web endpoint:
@app.get("/user")
def get_user(user: dict = Depends(user_dep)) -> dict:
    return user

如果你的依赖函数只是检查某些事情,并不返回任何值,你也可以在路径装饰器中定义这个依赖,也就是前面那一行以 @ 开头的代码:

@app.method(url, dependencies=[Depends(depfunc)])

让我们在示例 6-3 中试一下。

示例 6-3 定义一个用户检查依赖

from fastapi import FastAPI, Depends, Params

app = FastAPI()

# the dependency function:
def check_dep(name: str = Params, password: str = Params):
    if not name:
        raise

# the path function / web endpoint:
@app.get("/check_user", dependencies=[Depends(check_dep)])
def check_user() -> bool:
    return True

多个路径

第 9 章会更详细地介绍如何构造一个更大的 FastAPI 应用程序,包括在顶层应用程序下面定义多个 router 对象,而不是把每个端点都挂到顶层应用程序上。示例 6-4 勾勒了这个思路。

示例 6-4 定义一个子路由依赖

from fastapi import FastAPI, Depends, APIRouter

router = APIRouter(..., dependencies=[Depends(depfunc)])

这会导致 router 下的所有路径函数都会调用 depfunc()

全局

当你定义顶层 FastAPI 应用程序对象时,可以向它添加依赖,这些依赖会应用于它的所有路径函数,如示例 6-5 所示。

示例 6-5 定义应用级依赖

from fastapi import FastAPI, Depends

def depfunc1():
    pass

def depfunc2():
    pass

app = FastAPI(dependencies=[Depends(depfunc1), Depends(depfunc2)])

@app.get("/main")
def get_main():
    pass

在这个例子中,你使用 pass 来忽略其他细节,只是为了展示如何附加依赖。

回顾

本章讨论了依赖和依赖注入——也就是在你需要数据的时候,用一种直接的方式获得所需数据的方法。下一章即将登场:Flask、Django 和 FastAPI 走进一家酒吧……