三周精通FastAPI:20 Dependencies 依赖项 和类作为依赖项

官方文档:依赖项 - FastAPI

依赖项

FastAPI 提供了简单易用,但功能强大的依赖注入系统。

这个依赖系统设计的简单易用,可以让开发人员轻松地把组件集成至 FastAPI

什么是「依赖注入」

编程中的「依赖注入」是声明代码(本文中为路径操作函数 )运行所需的,或要使用的「依赖」的一种方式。

然后,由系统(本文中为 FastAPI)负责执行任意需要的逻辑,为代码提供这些依赖(「注入」依赖项)。

依赖注入常用于以下场景:

  • 共享业务逻辑(复用相同的代码逻辑)
  • 共享数据库连接
  • 实现安全、验证、角色权限
  • 等……

上述场景均可以使用依赖注入,将代码重复最小化。

第一步

接下来,我们学习一个非常简单的例子,尽管它过于简单,不是很实用。

但通过这个例子,您可以初步了解「依赖注入」的工作机制。

创建依赖项

首先,要关注的是依赖项。依赖项就是一个函数,且可以使用与路径操作函数相同的参数:

from typing import Union

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(
    q: Union[str, None] = None, skip: int = 0, limit: int = 100
):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

大功告成。

只用了2 行代码。

依赖项函数的形式和结构与路径操作函数一样。

因此,可以把依赖项当作没有「装饰器」(即,没有 @app.get("/some-path") )的路径操作函数。

依赖项可以返回各种内容。

本例中的依赖项预期接收如下参数:

  • 类型为 str 的可选查询参数 q
  • 类型为 int 的可选查询参数 skip,默认值是 0
  • 类型为 int 的可选查询参数 limit,默认值是 100

然后,依赖项函数返回包含这些值的 dict

导入 Depends

from fastapi import Depends, FastAPI

声明依赖项

与在路径操作函数参数中使用 BodyQuery 的方式相同,声明依赖项需要使用 Depends 和一个新的参数:

@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

虽然,在路径操作函数的参数中使用 Depends 的方式与 BodyQuery 相同,但 Depends 的工作方式略有不同。

这里只能传给 Depends 一个参数。

且该参数必须是可调用对象,比如函数。

该函数接收的参数和路径操作函数的参数一样。

"提示"

下一章介绍,除了函数还有哪些「对象」可以用作依赖项。

接收到新的请求时,FastAPI 执行如下操作:

  • 用正确的参数调用依赖项函数(「可依赖项」)
  • 获取函数返回的结果
  • 把函数返回的结果赋值给路径操作函数的参数

common_parameters

/items/

/users/

这样,只编写一次代码,FastAPI 就可以为多个路径操作共享这段代码 。

"检查"

注意,无需创建专门的类,并将之传递给 FastAPI 以进行「注册」或执行类似的操作。

只要把它传递给 DependsFastAPI 就知道该如何执行后续操作。

要不要使用 async

FastAPI 调用依赖项的方式与路径操作函数一样,因此,定义依赖项函数,也要应用与路径操作函数相同的规则。

即,既可以使用异步的 async def,也可以使用普通的 def 定义依赖项。

在普通的 def 路径操作函数中,可以声明异步的 async def 依赖项;也可以在异步的 async def 路径操作函数中声明普通的 def 依赖项。

上述这些操作都是可行的,FastAPI 知道该怎么处理。

"笔记"

如里不了解异步,请参阅异步:“着急了?” 一章中 async 和 await 的内容。

与 OpenAPI 集成

依赖项及子依赖项的所有请求声明、验证和需求都可以集成至同一个 OpenAPI 概图。

所以,交互文档里也会显示依赖项的所有信息:

简单用法

观察一下就会发现,只要路径 和操作匹配,就可以使用声明的路径操作函数。然后,FastAPI 会用正确的参数调用函数,并提取请求中的数据。

实际上,所有(或大多数)网络框架的工作方式都是这样的。

开发人员永远都不需要直接调用这些函数,这些函数是由框架(在此为 FastAPI )调用的。

通过依赖注入系统,只要告诉 FastAPI 路径操作函数 还要「依赖」其他在路径操作函数之前执行的内容,FastAPI 就会执行函数代码,并「注入」函数返回的结果。

其他与「依赖注入」概念相同的术语为:

  • 资源(Resource)
  • 提供方(Provider)
  • 服务(Service)
  • 可注入(Injectable)
  • 组件(Component)

FastAPI 插件

依赖注入系统支持构建集成和「插件」。但实际上,FastAPI 根本不需要创建「插件」,因为使用依赖项可以声明不限数量的、可用于路径操作函数的集成与交互。

创建依赖项非常简单、直观,并且还支持导入 Python 包。毫不夸张地说,只要几行代码就可以把需要的 Python 包与 API 函数集成在一起。

下一章将详细介绍在关系型数据库、NoSQL 数据库、安全等方面使用依赖项的例子。

FastAPI 兼容性

依赖注入系统如此简洁的特性,让 FastAPI 可以与下列系统兼容:

  • 关系型数据库
  • NoSQL 数据库
  • 外部支持库
  • 外部 API
  • 认证和鉴权系统
  • API 使用监控系统
  • 响应数据注入系统
  • 等等……

简单而强大

虽然,层级式依赖注入系统的定义与使用十分简单,但它却非常强大。

比如,可以定义依赖其他依赖项的依赖项。

最后,依赖项层级树构建后,依赖注入系统会处理所有依赖项及其子依赖项,并为每一步操作提供(注入)结果。

比如,下面有 4 个 API 路径操作(端点):

  • /items/public/
  • /items/private/
  • /users/{user_id}/activate
  • /items/pro/

开发人员可以使用依赖项及其子依赖项为这些路径操作添加不同的权限:

current_user

active_user

admin_user

paying_user

/items/public/

/items/private/

/users/{user_id}/activate

/items/pro/

与 OpenAPI 集成

在声明需求时,所有这些依赖项还会把参数、验证等功能添加至路径操作。

FastAPI 负责把上述内容全部添加到 OpenAPI 概图,并显示在交互文档中。

类作为依赖项

在深入探究 依赖注入 系统之前,让我们升级之前的例子。

来自前一个例子的dict

在前面的例子中, 我们从依赖项 ("可依赖对象") 中返回了一个 dict:

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

但是后面我们在路径操作函数的参数 commons 中得到了一个 dict

我们知道编辑器不能为 dict 提供很多支持(比如补全),因为编辑器不知道 dict 的键和值类型。

对此,我们可以做的更好...

什么构成了依赖项?

到目前为止,您看到的依赖项都被声明为函数。

但这并不是声明依赖项的唯一方法(尽管它可能是更常见的方法)。

关键因素是依赖项应该是 "可调用对象"。

Python 中的 "可调用对象" 是指任何 Python 可以像函数一样 "调用" 的对象。

所以,如果你有一个对象 something (可能不是一个函数),你可以 "调用" 它(执行它),就像:

something()

或者

something(some_argument, some_keyword_argument="foo")

这就是 "可调用对象"。

类作为依赖项

您可能会注意到,要创建一个 Python 类的实例,您可以使用相同的语法。举个例子:

class Cat:
    def __init__(self, name: str):
        self.name = name


fluffy = Cat(name="Mr Fluffy")

在这个例子中, fluffy 是一个 Cat 类的实例。

为了创建 fluffy,你调用了 Cat 。

所以,Python 类也是 可调用对象

因此,在 FastAPI 中,你可以使用一个 Python 类作为一个依赖项。

实际上 FastAPI 检查的是它是一个 "可调用对象"(函数,类或其他任何类型)以及定义的参数。

如果您在 FastAPI 中传递一个 "可调用对象" 作为依赖项,它将分析该 "可调用对象" 的参数,并以处理路径操作函数的参数的方式来处理它们。包括子依赖项。

这也适用于完全没有参数的可调用对象。这与不带参数的路径操作函数一样。所以,我们可以将上面的依赖项 "可依赖对象" common_parameters 更改为类 CommonQueryParams:

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

注意用于创建类实例的 __init__ 方法:

class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit

...它与我们以前的 common_parameters 具有相同的参数:

async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}

这些参数就是 FastAPI 用来 "处理" 依赖项的。

在两个例子下,都有:

  • 一个可选的 q 查询参数,是 str 类型。
  • 一个 skip 查询参数,是 int 类型,默认值为 0
  • 一个 limit 查询参数,是 int 类型,默认值为 100

在两个例子下,数据都将被转换、验证、在 OpenAPI schema 上文档化,等等。

使用它

现在,您可以使用这个类来声明你的依赖项了。

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

FastAPI 调用 CommonQueryParams 类。这将创建该类的一个 "实例",该实例将作为参数 commons 被传递给你的函数。

类型注解 vs Depends

注意,我们在上面的代码中编写了两次CommonQueryParams

commons: CommonQueryParams = Depends(CommonQueryParams) 

最后的 CommonQueryParams:

... = Depends(CommonQueryParams) 

...实际上是 Fastapi 用来知道依赖项是什么的。

FastAPI 将从依赖项中提取声明的参数,这才是 FastAPI 实际调用的。


在本例中,第一个 CommonQueryParams :

commons: CommonQueryParams ... 

...对于 FastAPI 没有任何特殊的意义。FastAPI 不会使用它进行数据转换、验证等 (因为对于这,它使用 = Depends(CommonQueryParams))。

你实际上可以只这样编写:

commons = Depends(CommonQueryParams) 

..就像:

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons=Depends(CommonQueryParams)):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

但是声明类型是被鼓励的,因为那样你的编辑器就会知道将传递什么作为参数 commons ,然后它可以帮助你完成代码,类型检查,等等:

快捷方式

但是您可以看到,我们在这里有一些代码重复了,编写了CommonQueryParams两次:

commons: CommonQueryParams = Depends(CommonQueryParams) 

FastAPI 为这些情况提供了一个快捷方式,在这些情况下,依赖项 明确地 是一个类,FastAPI 将 "调用" 它来创建类本身的一个实例。

对于这些特定的情况,您可以跟随以下操作:

不是写成这样:

commons: CommonQueryParams = Depends(CommonQueryParams) 

...而是这样写:

commons: CommonQueryParams = Depends() 

您声明依赖项作为参数的类型,并使用 Depends() 作为该函数的参数的 "默认" 值(在 = 之后),而在 Depends() 中没有任何参数,而不是在 Depends(CommonQueryParams) 编写完整的类。同样的例子看起来像这样:

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends()):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

... FastAPI 会知道怎么处理。

Tip

如果这看起来更加混乱而不是更加有帮助,那么请忽略它,你不需要它。

这只是一个快捷方式。因为 FastAPI 关心的是帮助您减少代码重复。

实践

源代码

将以下代码写入dependssample.py文件

from typing import Union

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(
    q: Union[str, None] = None, skip: int = 0, limit: int = 100
):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    
    return commons


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/itemsc/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items}) 
    commons.
    return response

@app.get("/itemsd/")
async def read_items(commons: CommonQueryParams = Depends()):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

启动服务

uvicorn dependssample:app --reload

测试

首先用浏览器浏览127.0.0.1:8000/docs 查看文档,可以看到已经创建好了几个GET路径。

在编辑器里查看代码建议和代码补全,观察itemsc和itemsd两个路径里的commons变量加上“点”之后代码提示。

使用浏览器或curl进行测试

浏览器地址:http://127.0.0.1:8000/items/?q=hello&skip=2

输出: 

{"q":"hello","skip":2,"limit":100}

curl get测试:

curl "127.0.0.1:8000/items/?q=hello&skip=8"
{"q":"hello","skip":8,"limit":100}

总结

声明依赖项,最简单的方法是声明一个函数,返回字典dict。但是为什么要用声明一个类的方法呢? 原来是字典无法被编辑器自动解析,也就是无法提供辅助编程建议(我们知道编辑器不能为 dict 提供很多支持(比如补全),因为编辑器不知道 dict 的键和值类型。),而类可以做的很好。比如下面例子,可以看到输入commons再输入“点”,编辑器会自动提醒三个参数:limit 、q和skip。这大约就是类作为依赖项的意义之一。

猜你喜欢

转载自blog.csdn.net/skywalk8163/article/details/143314895