fastapi基础教程(上)
fastapi基础教程(上)

fastapi基础教程(上)

1.安装

使用 FastAPI 需要 Python 版本大于等于 3.6

安装很简单,直接 pip install fastapi 即可,

并且会自动安装 Starlette 和 Pydantic。

然后还要 pip install uvicorn,因为 uvicorn 是运行相关应用程序的服务器。或者一步到位:

pip install fastapi[all]

会将所有依赖全部安装。

2.简单请求

编写main.py:

from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

@app.get("/hello/{name}")
async def say_hello(name: str):
    return {"message": f"Hello {name}"}

if __name__ == '__main__':
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

用pycharm自建的项目是这样的:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

@app.get("/hello/{name}")
async def say_hello(name: str):
    return {"message": f"Hello {name}"}

差在哪呢?就是uvicorn部分,其实也可以通过命令行启动服务,效果类似:

uvicorn test:app --reload
# 指定端口启动
uvicorn test:app --host '0.0.0.0' --port 8000 --reload

image-20221114144456033

2.1 不同类型的返回值

编写test_main.http测试请求:

test_main.http:

1).int

# Test your FastAPI endpoints

GET http://127.0.0.1:5555/str
Accept: application/json
http://127.0.0.1:5555/int

HTTP/1.1 200 OK
date: Mon, 14 Nov 2022 06:51:58 GMT
server: uvicorn
content-length: 3
content-type: application/json

666
响应文件已保存。
> 2022-11-14T145159.200.json

Response code: 200 (OK); Time: 3ms (3 ms); Content length: 3 bytes (3 B)

2).str

GET http://127.0.0.1:5555/str
Accept: application/json
http://127.0.0.1:5555/str

HTTP/1.1 200 OK
date: Mon, 14 Nov 2022 06:53:25 GMT
server: uvicorn
content-length: 14
content-type: application/json

"古明地觉"
响应文件已保存。
> 2022-11-14T145325.200.json

Response code: 200 (OK); Time: 1ms (1 ms); Content length: 6 bytes (6 B)

3).bytes

GET http://127.0.0.1:5555/bytes
Accept: application/json
http://127.0.0.1:5555/bytes

HTTP/1.1 200 OK
date: Mon, 14 Nov 2022 06:54:38 GMT
server: uvicorn
content-length: 8
content-type: application/json

"satori"
响应文件已保存。
> 2022-11-14T145439.200.json

Response code: 200 (OK); Time: 2ms (2 ms); Content length: 8 bytes (8 B)

4).tuple

GET http://127.0.0.1:5555/tuple
Accept: application/json
http://127.0.0.1:5555/tuple

HTTP/1.1 200 OK
date: Mon, 14 Nov 2022 06:55:34 GMT
server: uvicorn
content-length: 49
content-type: application/json

[
  "古明地觉",
  "古明地恋",
  "雾雨魔理沙"
]
响应文件已保存。
> 2022-11-14T145535.200.json

Response code: 200 (OK); Time: 3ms (3 ms); Content length: 23 bytes (23 B)

5).list

GET http://127.0.0.1:5555/list
Accept: application/json
http://127.0.0.1:5555/list

HTTP/1.1 200 OK
date: Mon, 14 Nov 2022 06:56:12 GMT
server: uvicorn
content-length: 67
content-type: application/json

[
  {
    "name": "古明地觉",
    "age": 17
  },
  {
    "name": "古明地恋",
    "age": 16
  }
]
响应文件已保存。
> 2022-11-14T145613.200.json

Response code: 200 (OK); Time: 2ms (2 ms); Content length: 51 bytes (51 B)

2.2 动态路径

可以看到基本上都是支持的,只不过元组自动转成列表返回了。并且当前的路径是写死的,如果我们想动态声明路径参数该怎么做呢?

from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/items/{item_id}")
async def get_item(item_id):
    """
    和 Flask 不同,Flask 是使用 <>
    而 FastAPI 使用 {}
    """
    return {"item_id": item_id}

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

测试路由:

GET http://127.0.0.1:5555/items/1
Accept: application/json
http://127.0.0.1:5555/items/1

HTTP/1.1 200 OK
date: Mon, 14 Nov 2022 06:59:03 GMT
server: uvicorn
content-length: 15
content-type: application/json

{
  "item_id": "1"
}
响应文件已保存。
> 2022-11-14T145903.200.json

Response code: 200 (OK); Time: 3ms (3 ms); Content length: 15 bytes (15 B)

2.3 参数类型限制

整体非常简单,路由里面的路径参数可以放任意个,只是 {} 里面的参数必须要在视图函数的参数中出现。但是问题来了,我们好像没有规定类型啊,如果我们希望某个路径参数只能接收指定的类型要怎么做呢?

正确类型:

# Test your FastAPI endpoints

GET http://127.0.0.1:5555/items/111
Accept: application/json
http://127.0.0.1:5555/items/111

HTTP/1.1 200 OK
date: Mon, 14 Nov 2022 07:28:57 GMT
server: uvicorn
content-length: 15
content-type: application/json

{
  "item_id": 111
}
响应文件已保存。
> 2022-11-14T152857.200.json

Response code: 200 (OK); Time: 1ms (1 ms); Content length: 15 bytes (15 B)

错误类型:

# Test your FastAPI endpoints

GET http://127.0.0.1:5555/items/qqq
Accept: application/json
http://127.0.0.1:5555/items/qqq

HTTP/1.1 422 Unprocessable Entity
date: Mon, 14 Nov 2022 07:27:37 GMT
server: uvicorn
content-length: 104
content-type: application/json

{
  "detail": [
    {
      "loc": [
        "path",
        "item_id"
      ],
      "msg": "value is not a valid integer",
      "type": "type_error.integer"
    }
  ]
}
响应文件已保存。
> 2022-11-14T152738.422.json

Response code: 422 (Unprocessable Entity); Time: 3ms (3 ms); Content length: 104 bytes (104 B)

如果我们传递的值无法转成整型的话,那么会进行提示:告诉我们 value 不是一个有效的整型,可以看到给的提示信息还是非常清晰的。

所以通过 Python 的类型声明,FastAPI 提供了数据校验的功能,当校验不通过的时候会清楚地指出没有通过的原因。在我们开发和调试的时候,这个功能非常有用。

2.4 路由顺序

我们在定义路由的时候需要注意一下顺序,举个例子:

from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/users/me")
async def read_user_me():
    return {"user_id": "the current user"}

@app.get("/users/{user_id}")
async def read_user(user_id: int):
    return {"user_id": user_id}

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

因为路径匹配是按照顺序进行的,所以这里要保证 /users/me/users/{user_id} 的前面,否则的话只会匹配到 /users/{user_id},这样的话访问 /users/me 就会解析错误,因为字符串 "me" 无法解析成整型。

3.交互式文档

FastAPI 会自动提供一个类似于 Swagger 的交互式文档,我们输入 localhost:5555/docs 即可进入。

image-20221114153247770

至于 localhost:5555/docs 页面本身,我们也是可以进行设置的:

from fastapi import FastAPI
import uvicorn

app = FastAPI(title="测试文档",
              description="这是一个简单的 demo",
              docs_url="/my_docs",
              openapi_url="/my_openapi")

@app.get("/items/{item_id}")
async def get_item(item_id: int):
    return {"item_id": item_id}

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

然后我们再重新进入,此时在浏览器里就需要输入 localhost:5555/my_docs:

image-20221114153704177

4.使用枚举

我们可以将某个路径参数通过类型注解的方式声明为指定的类型(准确的说是可以转成指定的类型,因为默认都是字符串),但如果我们希望它只能是规定的几个值之一,该怎么做呢?

from enum import Enum
from fastapi import FastAPI
import uvicorn

app = FastAPI()

class Name(str, Enum):
    satori = "古明地觉"
    koishi = "古明地恋"
    marisa = "雾雨魔理沙"

@app.get("/users/{user_name}")
async def get_user(user_name: Name):
    return {"user_id": user_name}

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

通过枚举的方式可以实现这一点,我们来测试一下:

图片

结果和我们期望的是一样的,可以再来看看 docs 生成的文档:

图片

可用的值都有哪些,也自动提示给我们了。

5. 路径包含 /

假设我们有这样一个路由:/files/{file_path},而用户传递的 file_path 中显然是可以带 / 的。假设file_path/root/test.py,那么路由就变成了 /files//root/test.py,显然这是有问题的。

@app.get("/users/{user_name}")
async def get_user(user_name: Name):
    return {"user_id": user_name}

image-20221115112355104

那么为了防止解析出错,我们需要做一个类似于 Flask 的操作:

from fastapi import FastAPI
import uvicorn

app = FastAPI()

# 声明 file_path 的类型为 path
# 这样它会被当成一个整体
@app.get("/files/{file_path:path}")
async def get_file(file_path: str):
    return {"file_path": file_path}

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

image-20221115112447781

结果没有问题,如果不将 file_path 的格式指定为 path,那么解析的时候就会找不到指定的路由。

6.查询参数

查询参数在 FastAPI 中依旧可以通过类型注解的方式进行声明,如果函数中定义了不属于路径参数的参数时,它们将会被解释为查询参数。

from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/users/{user_id}")
async def get_user(user_id: str, name: str, age: int):
    """
    我们在函数中定义了 user_id、name、age 三个参数
    显然 user_id 和 路径参数中的 user_id 对应
    然后 name 和 age 会被解释成查询参数
    这三个参数的顺序没有要求,但一般都是路径参数在前,查询参数在后
    """
    return {"user_id": user_id, "name": name, "age": age}

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

注意:name 和 age 没有默认值,这意味着它们是必须要传递的,否则报错。

image-20221115112940769

传入参数后:

image-20221115113024175

我们看到当不传递 name 和 age 的时候,会直接提示你相关的错误信息。如果我们希望用户可以不传递的话,那么必须要指定一个默认值。

@app.get("/users/{user_id}")
async def get_user(user_id: str, name: str = "UNKNOWN", age: int = 0):
    """
    我们在函数中定义了 user_id、name、age 三个参数
    显然 user_id 和 路径参数中的 user_id 对应
    然后 name 和 age 会被解释成查询参数
    这三个参数的顺序没有要求,但一般都是路径参数在前,查询参数在后
    """
    return {"user_id": user_id, "name": name, "age": age}

image-20221115113207940

我们看到,传递的 age 依旧需要整型,只不过在不传的时候会使用字符串类型的默认值。所以指定的类型和默认值类型不相同,也是允许的,只不过这么做显然是不合理的。

此外我们还可以指定多个类型,比如让 user_id 按照整型解析、解析不成功时退化为字符串。

from typing import Union, Optional
from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/users/{user_id}")
async def get_user(user_id: Union[int, str],
                   name: Optional[str] = None):
    """
    通过 Union 来声明一个混合类型,int 在前、str 在后
    会先按照 int 解析,解析失败再变成 str
    然后是 name,它表示字符串类型、但默认值为 None(不是字符串)
    那么应该声明为 Optional[str]
    """
    return {"user_id": user_id, "name": name}

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

image-20221115113900712

image-20221115113922748

所以 FastAPI 的设计还是非常不错的,通过 Python 的类型注解来实现参数类型的限定可以说是非常巧妙的,因此这也需要我们熟练掌握 Python 的类型注解。

这里补充一下,我当前的 Python 版本是 3.8,如果你用的是 3.10,那么类型注解还有不同的写法:

>>> from typing import Union, Optional
# Optional[str] 和 str | None 等价
>>> name: Optional[str] = "古明地觉"
>>> name: str | None = "古明地觉"
# Union[int, str] 和 int | str 等价
>>> age: Union[int, str] = 17
>>> age: int | str = 17

这种写法在 3.10 才开始正式引入,但通过 from __future__ import annotations 也可以在 3.9 里面使用,而 3.8 是不支持的。

6.1布尔类型自动转换

对于布尔类型,FastAPI 支持自动转换,举个例子:

@app.get("/{flag}")
async def get_flag(flag: bool):
    return {"flag": flag}

0/false/off/yes --- > {'flag': True}

1/true/on/no --- > {'flag': False}

6.2多个路径和查询参数

前面说过,可以定义任意个路径参数,只要动态的路径参数 {} 在函数的参数中都出现即可。当然查询参数也可以是任意个,FastAPI 可以处理的很好。

from typing import Union, Optional
from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/postgres/{schema}/v1/{table}")
async def get_data(schema: str,
                   table: str,
                   select: str = "*",
                   where: Optional[str] = None,
                   limit: Optional[int] = None,
                   offset: Optional[int] = None):
    """
    标准格式是:路径参数按照顺序在前,查询参数在后
    但 FastAPI 对顺序本身是没有什么要求的
    """
    query = f"select {select} from {schema}.{table}"
    print(query)
    if where:
        query += f" where {where}"
    if limit:
        query += f" limit {limit}"
    if offset:
        query += f" offset {offset}"
    return {"query": query}

if __name__ == "__main__":
from typing import Union, Optional
from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/postgres/{schema}/v1/{table}")
async def get_data(schema: str,
                   table: str,
                   select: str = "*",
                   where: Optional[str] = None,
                   limit: Optional[int] = None,
                   offset: Optional[int] = None):
    """
    标准格式是:路径参数按照顺序在前,查询参数在后
    但 FastAPI 对顺序本身是没有什么要求的
    """
    query = f"select {select} from {schema}.{table}"
    print(query)
    if where:
        query += f" where {where}"
    if limit:
        query += f" limit {limit}"
    if offset:
        query += f" offset {offset}"
    return {"query": query}

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555, reload=True)

6.3 依赖注入Depends

from typing import Optional
from fastapi import FastAPI, Depends
import uvicorn

app = FastAPI()

async def common_parameters(
        select: str = "*",
        skip: int = 0,
        limit: int = 100
):
    return {"select": select, "skip": skip, "limit": limit}

@app.get("/items")
async def read_items(
        commons: dict = Depends(common_parameters)):
    """
    common_parameters 接收三个参数:q、skip、limit
    因此会从请求中解析出 q、skip、limit 并传给 common_parameters
    然后将 common_parameters 的返回值赋给 commons
    但如果解析不到某个参数,那么会判断函数中参数是否有默认值
    没有的话就会返回错误
    """
    return commons

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555, reload=True)

image-20221115141031798


image-20221115141240832

7.数据校验(针对查询参数)

7.1 fastapi.Query

FastAPI 支持我们进行更加智能的数据校验,比如一个字符串,我们希望用户在传递的时候只能传递长度为 6 到 15 的字符串该怎么做呢?

from fastapi import Query

from typing import Optional
from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()

@app.get("/user")
async def check_length(
        password: Optional[str] = Query(None, min_length=6, max_length=15)
        ):
    """
    默认值为 None,应该声明为 Optional[str],当然声明 str 也是可以的
    只不过声明为 str,那么默认值应该也是 str
    所以如果允许一个类型的值为空,那么更规范的做法应该是声明为 Optional[类型]
    """

    return {"password": password}

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555, reload=True)

password 是可选的,但传递的时候必须传字符串、而且还是长度在 6 到 15 之间的字符串。所以在声明默认值的时候 None 和 Query(None) 是等价的,只不过 Query 还支持对参数进行额外的限制。

运行结果:

image-20221115170250230

Query 里面除了限制最小长度和最大长度,还有其它的功能:

from fastapi import FastAPI, Query
q = Query(None, min_length=6, max_length=15)

我们查看q的属性:

image-20221115170114245

更改点代码:

@app.get("/user")
async def check_length(
    password: str = Query("satori", min_length=6, 
                          max_length=15, regex=r"^satori")
):
    """
    此时 password 的默认值为 'satori',并且传递的时候也必须要以 'satori' 开头
    但值得注意的是 password 后面的类型注解是 str,不再是 Optional[str]
    因为默认值不是 None 了,当然这里即使写成 Optional[str] 也是没有什么影响的
    """
    return {"password": password}

image-20221115171035906


image-20221115171056947


image-20221115170857887


image-20221115170816721


7.2 声明查询参数为必传参数

如果我们想让某个查询参数为必传参数,只需要不给它默认值就行了。

函数里面的参数,要么是路径参数、要么是查询参数。显然 password 是一个查询参数,通过不指定默认值,我们即可实现它是一个必传参数。也就是在 URL 中,必须通过 ?password=xxx 的方式进行传递。

虽然目的很简单,但我们发现此时无法对 password 进行限制了,比如希望它的长度是 6 到 15。那么问题来了,如何才能两者兼顾呢?

from typing import Optional
from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()

@app.get("/user")
async def check_length(
        password: Optional[str] = Query(..., min_length=6, max_length=15)):
    """
    默认值为 None,应该声明为 Optional[str],当然声明 str 也是可以的
    只不过声明为 str,那么默认值应该也是 str
    所以如果允许一个类型的值为空,那么更规范的做法应该是声明为 Optional[类型]
    -----------------------------------------------------------------
    此时 password 的默认值为 'satori',并且传递的时候也必须要以 'satori' 开头
    但值得注意的是 password 后面的类型注解是 str,不再是 Optional[str]
    因为默认值不是 None 了,当然这里即使写成 Optional[str] 也是没有什么影响的
    """

    return {"password": password}

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555, reload=True)

image-20221115190736037


image-20221115190633816

... 是 Python 的一个特殊的对象,可以了解一下,在 Numpy 里面也可以使用它。

最后再补充一点,我们也可以不使用 Query,将 password 的长度限制逻辑写在函数体里面也是一样的。

7.3 同时获取多个相同的查询参数

如果我们指定了 a=1&a=2,那么在获取 a 的时候,会得到什么呢?

from typing import List
from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()

@app.get("/items")
async def read_items(
        a1: str = Query(...),
        a2: List[str] = Query(...),
        b: List[str] = Query(...)
):
    return {"a1": a1, "a2": a2, "b": b}

我们访问一下,看看结果:

image-20221115191138426


首先 a2 和 b 都是列表,会获取所有的值,但 a1 只获取了最后一个值。

另外可能有人觉得代码有点啰嗦,在函数声明中可不可以这样写呢?

@app.get("/items")
async def read_items(
        a1: str,
        a2: List[str],
        b: List[str]
):
    return {"a1": a1, "a2": a2, "b": b}

我们将 Query(...) 去掉了,因为它没有对参数做其它的限制,只是表示参数是一个必传参数。而不指定 Query(...),那么本身就是一个必传参数,所以完全可以把 Query(...) 去掉。

这种做法,对于 a1 来说是可行的,但对于 a2 和 b 来说不行。对于类型为 list 的查询参数,我们必须要显式的加上 Query(...) 来表示必传参数。如果允许为 None(或者有默认值)的话,那么应该这么写:

@app.get("/items")
async def read_items(
    a1: str,
    a2: Optional[List[str]] = Query(None),
    b: List[str] = Query(["1", "嘿嘿"])
):
    return {"a1": a1, "a2": a2, "b": b}

image-20221115191352054


7.4 给参数起别名

问题来了,假设我们定义的查询参数名叫 item-query,那么由于它要体现在函数参数中,而这显然不符合 Python 变量的命名规范,这个时候要怎么做呢?

Query(alias="***")

@app.get("/items")
async def read_items(
    # 三个查询参数,分别是 item-query、@@@@、$$$$
    # 但它们不符合 Python 变量的命名规范
    # 于是要为它们起别名
    item1: Optional[str] = Query(None, alias="item-query"),
    item2: str = Query("哈哈", alias="@@@@"),
    # item3 是必传的
    item3: str = Query(..., alias="$$$$")  
):
    return {"item-query": item1, "@@@@": item2, "$$$$": item3}

image-20221115191854373


7.5 数据检测

Query 不仅仅支持对字符串的校验,还支持对数值的校验,里面可以传递 gt, ge, lt, le 等参数,相信这几个参数不用说你也知道是干什么的,我们举例说明:

@app.get("/items")
async def read_items(
    # item1 必须大于 5
    item1: int = Query(..., gt=5),
    # item2 必须小于等于 7
    item2: int = Query(..., le=7),

    # item3 必须等于 10
    item3: int = Query(..., ge=10, le=10)
):
    return {"item1": item1, 
            "item2": item2, 
            "item3": item3}
  • gt:表示大于>。即greater than
  • ge:表示大于等于>=。即greater than or equals to
  • lt:表示小于<。即less than
  • le:表示小于等于<=。即less than or equals to
  • eq:表示等于=。即equals
  • ne:表示不等于!=。即not equals

image-20221115192330789


不符合条件的参数则会报错:

image-20221115192402454


Query 还是比较强大的 ,当然内部还有一些其它的参数是针对 docs 交互文档的,有兴趣可以自己了解一下。

8.数据校验(针对路径参数)

对查询参数进行数据校验使用的是 Query,对路径参数进行数据校验使用的是 Path,两者的使用方式一模一样,没有任何区别。

from fastapi import FastAPI, Path
import uvicorn

app = FastAPI()

@app.get("/items/{item-id}")
async def read_items(
    item_id: int = Path(..., alias="item-id")
):
    return {"item-id": item_id}

路径参数是必须的,它是路径的一部分,所以我们应该使用 ... 将其标记为必传参数。当然即使不标记也无所谓,就算指定了默认值也用不上,因为路径参数不指定压根就匹配不到相应的路由。至于一些其它的校验,和查询参数一模一样,所以这里不再赘述了。

不过我们之前说过,路径参数应该在查询参数的前面,尽管 FastAPI 没有这个要求,但是这样写明显更舒服一些。不过问题来了,如果路径参数需要指定别名,但是某一个查询参数不需要,这个时候就会出现问题:

@app.get("/items/{item-id}")
async def read_items(
    q: str,
    item_id: int = Path(..., alias="item-id")
):

    return {"item_id": item_id, "q": q}

显然此时 Python 的语法就决定了 item_id 必须放在 q 的后面,当然这么做是完全没有问题的,FastAPI 对参数的先后顺序没有任何要求,因为它是通过参数的名称、类型和默认值声明来检测参数,而不在乎参数的顺序。但如果我们就要让 item_id 在 q 的前面要怎么做呢?

@app.get("/items/{item-id}")
async def read_items(
    *,
    item_id: int = Path(..., alias="item-id"),
    q: str,
):

    return {"item_id": item_id, "q": q}

此时就没有问题了,通过将第一个参数设置为 *,使得 item_id 和 q 都必须通过关键字参数传递,所以此时默认参数在非默认参数之前也是允许的。当然我们也不需要担心 FastAPI 传参的问题,你可以认为它所有的参数都是通过关键字参数的方式传递的。

发表回复

您的电子邮箱地址不会被公开。