FastAPI:Python 世界里最受歡迎的異步框架


楔子

這次我們來聊一聊 FastAPI,它和我們之前介紹的 Sanic 是類似的,都是 Python 中的異步 web 框架。相比 Sanic,FastAPI 更加的成熟、社區也更加的活躍,那么 FastAPI 都有哪些特點呢?

  • 快速:擁有非常高的性能,歸功於 Starlette 和 Pydantic;Starlette 用於路由匹配,Pydantic 用於數據驗證
  • 開發效率:功能開發效率提升 200% 到 300%
  • 減少 bug:減少 40% 的因為開發者粗心導致的錯誤
  • 智能:內部的類型注解非常完善,編輯器可處處自動補全
  • 簡單:框架易於使用,文檔易於閱讀
  • 簡短:使代碼重復最小化,通過不同的參數聲明實現豐富的功能
  • 健壯:可以編寫出線上使用的代碼,並且會自動生成交互式文檔
  • 標准化:兼容 API 相關開放標准

FastAPI 最大的特點就是它使用了 Python 的類型注解,我們后面會詳細說,下面來安裝一下 FastAPI。

使用 FastAPI 需要 Python 版本大於等於 3.6。

首先是 pip install fastapi,會自動安裝 Starlette 和 Pydantic;然后還要 pip install uvicorn,因為 uvicorn 是運行相關應用程序的服務器。或者一步到胃:pip install fastapi[all],會將所有依賴全部安裝。

請求與響應

我們來使用 FastAPI 編寫一個簡單的應用程序:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
import uvicorn

# 類似於 app = Flask(__name__)
app = FastAPI()

# 綁定路由和視圖函數
@app.get("/")
async def index():
    return {"name": "古明地覺"}


# 在 Windows 中必須加上 if __name__ == "__main__",否則會拋出 RuntimeError: This event loop is already running
if __name__ == "__main__":
    # 啟動服務,因為我們這個文件叫做 main.py,所以需要啟動 main.py 里面的 app
    # 第一個參數 "main:app" 就表示這個含義,然后是 host 和 port 表示監聽的 ip 和端口
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

整個過程顯然很簡單,然后我們我們在瀏覽器中輸入 "localhost:5555" 就會顯示相應的輸出,我們看到在視圖函數中可以直接返回一個字典。當然除了字典,其它的數據類型也是可以的,舉個栗子:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/int")
async def index1():
    return 666

@app.get("/str")
async def index2():
    return "古明地覺"

@app.get("/bytes")
async def index3():
    return b"satori"

@app.get("/tuple")
async def index4():
    return ("古明地覺", "古明地戀", "霧雨魔理沙")

@app.get("/list")
async def index5():
    return [{"name": "古明地覺", "age": 17}, {"name": "古明地戀", "age": 16}]


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

這里我們直接使用 requests 發請求:

我們看到基本上都是支持的,只不過元組自動轉成列表返回了。這里我們在路由中指定了路徑,可以看到 FastAPI 中的路徑形式和其它框架並無二致,只不過目前的路徑是寫死的,如果我們想動態聲明路徑參數該怎么做呢?

# -*- coding:utf-8 -*-
# @Author: komeiji satori
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)

整體非常簡單,路由里面的路徑參數我們可以放任意個,只是 {} 里面的參數必須要在定義的視圖函數的參數中出現。但是問題來了,我們好像沒有規定類型啊,如果我們希望某個路徑參數只能接收指定的類型要怎么做呢?

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/apple/{item_id}")
async def get_item(item_id: int):
    """和 Flask 不同,Flask 定義類型是在路由當中,也就是在 <> 里面,變量和類型通過 : 分隔
       而 FastAPI 是使用類型注解的方式,此時的 item_id 要求一個整型(准確的說是一個能夠轉成整型的字符串)"""
    return {"item_id": item_id}

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

如果我們傳遞的值無法轉成整型的話,那么會進行提示:告訴我們 value 不是一個有效的整型,可以看到給的提示信息還是非常清晰的。

所以通過 Python 的類型聲明,FastAPI提供了數據校驗的功能,當校驗不通過的時候會清楚地指出沒有通過的原因。在我們開發和調試的時候,這個功能非常有用。

交互式文檔

FastAPI 會自動提供一個類似於 Swagger 的交互式文檔,我們輸入 "localhost:5555/docs" 即可進入。

有興趣可以自己嘗試測試一下,然后我們注意一下里面的 /openapi.json,我們可以點擊進去,會發現里面包含了我們定義的路由信息。

至於 "localhost:5555/docs" 頁面本身,我們也是可以進行設置的:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
import uvicorn

app = FastAPI(title="測試文檔",
              description="這是一個簡單的 demo",
              docs_url="/my_docs",
              openapi_url="/my_openapi")

@app.get("/apple/{item_id}")
async def get_item(item_id: int):
    """和 Flask 不同,Flask 定義類型是在路由當中,也就是在 <> 里面,變量和類型通過 : 分隔
       而 FastAPI 是使用類型注解的方式,此時的 item_id 要求一個整型(准確的說是一個能夠轉成整型的字符串)"""
    return {"item_id": item_id}

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

然后我們再重新進入,此時就需要通過 "localhost:5555/my_docs":

整體沒什么難度,我們還可以指定其它參數,比如 version 表示版本,可以自己試試。該頁面主要用來測試自己編寫的 API 服務,不過個人更喜歡使用 requests 發請求。

路由順序

然后我們在定義路由的時候需要注意一下順序,舉個栗子:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
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" 無法解析成整型。

使用枚舉

我們可以將某個路徑參數通過類型注解的方式聲明為指定的類型(准確的說是可以轉成指定的類型,因為默認都是字符串),但如果我們希望它只能是我們規定的幾個值之一該怎么做呢?

# -*- coding:utf-8 -*-
# @Author: komeiji satori
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 生成的文檔:

提示我們,可以用的值:古明地覺、古明地戀、霧雨魔理沙。

路徑中包含 /

假設我們有這樣一個路由:/files/{file_path},而用戶傳遞的 file_path 中顯然是可以帶 / 的,假設 file_path 是 /root/test.py,那么路由就變成了 /files//root/test.py,顯然這是有問題的。

那么為了防止解析出錯,我們需要做一個類似於 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)

然后我們來訪問一下:

查詢參數

查詢參數在 FastAPI 中依舊可以通過類型注解的方式進行聲明,如果函數中定義了不屬於路徑參數的參數時,那么它們將會被自動解釋會查詢參數。

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/user/{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 是沒有默認值的,這意味着它們是必須要傳遞的,否則報錯。

我們看到當不傳遞 name 和 age 的時候,會直接提示你相關的錯誤信息。如果我們希望用戶可以不傳遞的話,那么必須要指定一個默認值。

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/user/{user_id}")
async def get_user(user_id: str, name: str = "UNKNOWN", age: int = 0):
    return {"user_id": user_id, "name": name, "age": age}


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

對於查詢參數,由於它們指定了類型,所以我們也要傳遞正確類型的數據。假設這里的 age 傳遞了一個 "abc",那么也是通不過的。

如果默認值和類型不相同怎么辦?

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/user/{user_id}")
async def get_user(user_id: str, name: str = "UNKNOWN", age: int = "哈哈哈"):
    return {"user_id": user_id, "name": name, "age": age}


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

這里的 age 需要接收一個整型,但是默認值卻是一個字符串,那么此時會有什么情況發生呢?我們來試一下:

我們看到,傳遞的 age 依舊需要整型,只不過在不傳的時候會使用字符串類型的默認值,所以類型和默認值類型不同的時候也是可以的,只不過這么做顯然是不合理的。但是問題來了,我們可不可以指定多個類型呢?比如 user_id 按照整型解析、解析不成功退化為字符串。

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from typing import Union, Optional
from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/user/{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)

所以 FastAPI 的設計還是非常不錯的,通過 Python 的類型注解來實現參數類型的限定可以說是非常巧妙的,因此這也需要我們熟練掌握 Python 的類型注解。

bool 類型自動轉換

對於布爾類型,FastAPI 支持自動轉換,舉個栗子:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
import uvicorn

app = FastAPI()

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


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

多個路徑和查詢參數

我們之前說過,可以定義任意個路徑參數,只要動態的路徑參數{} 里面的 在函數的參數中都出現即可。當然查詢參數也可以是任意個,FastAPI 可以處理的很好。

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from typing import 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):
    """標准格式是:路徑參數按照順序在前,查詢參數在后
       但其實對順序是沒有什么要求的"""
    query = f"select {select} from {schema}.{table}"
    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)

然后我們使用 requests 來測試一下:

print(
    requests.get("http://localhost:5555/postgres/ods/v1/staff").json()
)  # {'query': 'select * from ods.staff'}
print(
    requests.get("http://localhost:5555/postgres/ods/v1/staff?select=id, name&where=id > 3&limit=100").json()
)  # {'query': 'select id, name from ods.staff where id > 3 limit 100'}

Depends

這個老鐵比較特殊,它是用來做什么的呢?我們來看一下:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from typing import Optional
import uvicorn
from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: Optional[str] = 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)):
    # common_parameters 接收三個參數:q、skip、limit
    # 然后在解析請求的時候,會將 q、skip、limit 傳遞到 common_parameters 中,然后將返回值賦值給 commons
    # 但如果解析不到某個參數時,那么會判斷函數中參數是否有默認值,沒有的話就會返回錯誤,而不是傳遞一個 None 進去
    return commons


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


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

我們來測試一下:

所以 Depends 能夠很好的實現依賴注入,而且我們特意寫了兩個路由,就是想表示它們是彼此獨立的。因此當有共享的邏輯、或者共享的數據庫連接、增強安全性、身份驗證、角色權限等等,會非常的實用。

查詢參數和數據校驗

FastAPI 支持我們進行更加智能的數據校驗,比如一個字符串,我們希望用戶在傳遞的時候只能傳遞長度為 6 到 15 的字符串該怎么做呢?

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from typing import Optional
from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()

@app.get("/user")
async def check_length(
        # 默認值為 None,應該聲明為 Optional[str],當然聲明 str 也是可以的。只不過聲明為 str,那么默認值應該也是 str
        # 所以如果一個類型允許為空,那么更規范的做法應該是聲明為 Optional[類型]。
        password: Optional[str] = Query(None, min_length=6, max_length=15)
):
    return {"password": password}


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

password 是可選的,但是一旦傳遞則必須傳遞字符串、而且還是長度在 6 到 15 之間的字符串。所以如果傳遞的是 None,那么在聲明默認值的時候 None 和 Query(None) 是等價的,只不過 Query 還支持其它的參數來對參數進行限制。

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

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()


@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}


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

聲明為必須參數

我們通過 Query 可以限制參數的長度,但是問題來了,這個時候我還希望這個參數是必傳的該怎么做呢?

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()

@app.get("/user")
async def check_length(
        password: str = Query(..., min_length=6)
):
    """將第一個參數換成 ... 即可實現該參數是必傳參數
    """
    return {"password": password}

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

... 是 Python 中的一個特殊的對象,可以了解一下,通過它可以實現該參數是必傳參數。

查詢參數變成一個列表

如果我們指定了 a=1&a=2,那么我們在獲取 a 的時候如何才能得到一個列表呢?

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from typing import Optional, 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}


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

我們訪問一下,看看結果:

首先 "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}

對於 a1 是可以的,但是 a2 和 b 不行。對於類型為 list 的查詢參數,無論有沒有默認值,你都必須要顯式的加上 Query 來表示必傳參數。如果允許為 None(或者有默認值)的話,那么應該這么寫:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from typing import Optional, List
from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()


@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}


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

給參數起別名

問題來了,假設我們定義的查詢參數名叫 item-query,那么由於它要體現在函數參數中、而這顯然不符合 Python 變量的命名規范,這個時候要怎么做呢?答案是起一個別名。

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from typing import Optional, List
from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()

@app.get("/items")
async def read_items(
        # 通過 url 的時候使用別名即可
        item1: Optional[str] = Query(None, alias="item-query"),
        item2: str = Query("哈哈", alias="@@@@"),
        item3: str = Query(..., alias="$$$$")  # item3 是必傳的
):
    return {"item1": item1, "item2": item2, "item3": item3}

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

數值檢測

Query 不僅僅支持對字符串的校驗,還支持對數值的校驗,里面可以傳遞 gt、ge、lt、le 這幾個參數,相信這幾個參數不用說你也知道是干什么的,我們舉例說明:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()

@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}

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

Query 還是比較強大了,當然內部還有一些其它的參數是針對 docs 交互文檔的,有興趣可以自己了解一下。

路徑參數和數據校驗

查詢參數數據校驗使用的是 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}

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

因為路徑參數是必須的,它是路徑的一部分,所以我們應該使用 ... 將其標記為必傳參數。當然即使不這么做也無所謂,因為指定了默認值也用不上,因為路徑參數不指定壓根就匹配不到相應的路由。至於一些其它的校驗,和查詢參數一模一樣,所以這里不再贅述了。

不過我們之前說過,路徑參數應該在查詢參數的前面,盡管 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 傳參的問題,你可以認為它所有的參數都是通過關鍵字參數的方式傳遞的。

Request

Request 是什么?首先我們知道任何一個請求都對應一個 Request 對象,請求的所有信息都在這個 Request 對象中,FastAPI 也不例外。

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI, Request
import uvicorn

app = FastAPI()


@app.get("/girl/{user_id}")
async def read_girl(user_id: str,
                    request: Request):
    """路徑參數是必須要體現在參數中,但是查詢參數可以不寫了
       因為我們定義了 request: Request,那么請求相關的所有信息都會進入到這個 Request 對象中"""
    header = request.headers  # 請求頭
    method = request.method  # 請求方法
    cookies = request.cookies  # cookies
    query_params = request.query_params  # 查詢參數
    return {"name": query_params.get("name"), "age": query_params.get("age"), "hobby": query_params.getlist("hobby")}


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

我們通過 Request 對象可以獲取所有請求相關的信息,我們之前當參數傳遞不對的時候,FastAPI 會自動幫我們返回錯誤信息,但通過 Request 我們就可以自己進行解析、自己指定返回的錯誤信息了。

Response

既然有 Request,那么必然會有 Response,盡管我們可以直接返回一個字典,但 FastAPI 實際上會幫我們轉成一個 Response 對象。

Response 內部接收如下參數:

  • content:返回的數據
  • status_code:狀態碼
  • headers:返回的請求頭
  • media_type:響應類型(就是 HTML 中 Content-Type,只不過這里換了個名字)
  • background:接收一個任務,Response 在返回之后會自動異步執行(這里不做介紹,后面會說)

舉個栗子:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI, Request, Response
import uvicorn
import orjson

app = FastAPI()


@app.get("/girl/{user_id}")
async def read_girl(user_id: str,
                    request: Request):
    query_params = request.query_params  # 查詢參數
    data = {"name": query_params.get("name"), "age": query_params.get("age"), "hobby": query_params.getlist("hobby")}
    # 實例化一個 Response 對象
    response = Response(
        # content,我們需要手動轉成 json 字符串,如果直接返回字典的話,那么在包裝成 Response 對象的時候會自動幫你轉
        orjson.dumps(data),
        # status_code,狀態碼
        201,
        # headers,響應頭
        {"Token": "xxx"},
        # media_type,就是 HTML 中的 Content-Type
        "application/json",  
    )
    # 如果想設置 cookie 的話,那么通過 response.set_cookie 即可
    # 刪除 cookie 則是 response.delete_cookie
    return response


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

通過 Response 我們可以實現請求頭、狀態碼、cookie 等自定義。

另外除了 Response 之外還有很多其它類型的響應,它們都在 fastapi.responses 中,比如:FileResponse、HTMLResponse、PlainTextResponse 等等。它們都繼承了 Response,只不過會自動幫你設置響應類型,舉個栗子:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
from fastapi.responses import Response, HTMLResponse
import uvicorn

app = FastAPI()


@app.get("/index")
async def index():

    response1 = HTMLResponse("<h1>你好呀</h1>")
    response2 = Response("<h1>你好呀</h1>", media_type="text/html")
    # 以上兩者是等價的,在 HTMLResponse 中會自動將 media_type 設置成 text/html
    return response1


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

其它類型的請求與響應

FastAPI 除了 GET 請求之外,還支持其它類型,比如:POST、PUT、DELETE、OPTIONS、HEAD、PATCH、TRACE 等等。而常見的也就 GET、POST、PUT、DELETE,介紹完了 GET,我們來說一說其它類型的請求。

顯然對應 POST、PUT 等類型的請求,我們必須要能夠解析出請求體,並且能夠構造出響應體。

Model

在 FastAPI 中,請求體和響應體都對應一個 Model,舉個栗子:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from typing import Optional, List
from fastapi import FastAPI, Request, Response
from pydantic import BaseModel
import uvicorn

app = FastAPI()


class Girl(BaseModel):
    """數據驗證是通過 pydantic 實現的,我們需要從中導入 BaseModel,然后繼承它"""
    name: str
    age: Optional[str] = None
    length: float
    hobby: List[str]  # 對於 Model 中的 List[str] 我們不需要指定 Query(准確的說是 Field)


@app.post("/girl")
async def read_girl(girl: Girl):
    # girl 就是我們接收的請求體,它需要通過 json 來傳遞,並且這個 json 要有上面的四個字段(age 可以沒有)
    # 通過 girl.xxx 的方式我們可以獲取和修改內部的所有屬性
    return dict(girl)  # 直接返回 Model 對象也是可以的


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

除了使用這種方式之外,我們還可以使用之前說的 Request 對象:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI, Request
import uvicorn

app = FastAPI()

@app.post("/girl")
async def read_girl(request: Request):
    # 是一個協程,所以需要 await
    data = await request.body()
    print(data)

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

首先我們在使用 requests 模塊發送 post 請求的時候可以通過 data 參數傳遞、也可以通過 json 參數。

當通過 json={"name": "satori", "age": 16, "length": 155.5} 傳遞的時候,會將其轉成 json 字符串進行傳輸,程序中的 print 打印如下:

b'{"name": "satori", "age": 16, "length": 155.5}'

如果我們是用過 data 參數發請求的話(值不變),那么會將其拼接成 k1=v1&k2=v2 的形式再進行傳輸(相當於表單提交,后面說),程序中打印如下:

b'name=satori&age=16&length=155.5'

所以我們看到 await request.body() 得到的就是最原始的字節流,而除了 await request.body() 之外還有一個 await request.json(),只是后者在內部在調用了前者拿到字節流之后、自動幫你 loads 成了字典。因此使用 await request.json() 也側面要求,我們必須在發送請求的時候必須使用 json 參數傳遞(傳遞的是字典轉成的 json、所以也能解析成字典),否則使用 await request.json() 是無法正確解析的。

路徑參數、查詢參數、請求體

我們可以將三者混合在一起:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from typing import Optional, List
from fastapi import FastAPI, Request, Response
from pydantic import BaseModel
import uvicorn

app = FastAPI()


class Girl(BaseModel):
    name: str
    age: Optional[str] = None
    length: float
    hobby: List[str]


@app.post("/girl/{user_id}")
async def read_girl(user_id,
                    q: str,
                    girl: Girl):

    return {"user_id": user_id, "q": q, **dict(girl)}


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

里面我們同時指定了路徑參數、查詢參數和請求體,FastAPI 依然是可以正確區分的,當然我們也可以使用 Request 對象。

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from typing import Optional, List, Dict
from fastapi import FastAPI, Request, Response
from pydantic import BaseModel
import uvicorn

app = FastAPI()

@app.post("/girl/{user_id}")
async def read_girl(user_id,
                    request: Request):

    q = request.query_params.get("q")
    data: Dict = await request.json()
    data.update({"user_id": user_id, "q": q})
    return data


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

可以自己測試一下,仍然是可以正確返回的。

多個請求體參數

我們上面的只接收一個 json 請求體,如果是接收兩個呢?

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from typing import Optional, List
from fastapi import FastAPI, Request, Response
from pydantic import BaseModel
import uvicorn

app = FastAPI()


class Girl(BaseModel):
    name: str
    age: Optional[str] = None


class Boy(BaseModel):
    name: str
    age: int


@app.post("/boy_and_girl")
async def read_boy_and_girl(girl: Girl,
                            boy: Boy):
    return {"girl": dict(girl), "boy": dict(boy)}


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

此時在傳遞的時候,應該按照如下方式傳遞:

應該將兩個 json 嵌套在一起,組成一個更大的 json,至於 key 就是我們的函數參數名。因此這種方式其實就等價於:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from typing import Optional, List, Dict
from fastapi import FastAPI, Request, Response
from pydantic import BaseModel
import uvicorn

app = FastAPI()

class BoyAndGirl(BaseModel):
    girl: Dict
    boy: Dict

@app.post("/boy_and_girl")
async def read_boy_and_girl(boy_and_girl: BoyAndGirl):
    return dict(boy_and_girl)

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

這種方式也是可以實現的,只不過就字典內部的字典的不可進行限制了。當然啦,我們仍然可以使用 Request 對象,得到字典之后自己再進行判斷,因為對於 json 而言,內部的字段可能是會變的,而且最關鍵的是字段可能非常多。這個時候,我個人更傾向於使用 Request 對象。

Form 表單

我們調用 requests.post,如果參數通過 data 傳遞的話,則相當於提交了一個 form 表單,那么在 FastAPI 中可以通過 await request.form() 進行獲取,注意:內部同樣是先調用 await request.body()。

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI, Request, Response
import uvicorn

app = FastAPI()

@app.post("/girl")
async def girl(request: Request):
    # 此時 await request.json() 報錯,因為是通過 data 參數傳遞的,相當於 form 表單提交
    # 如果是通過 json 參數傳遞,那么 await request.form() 會得到一個空表單
    form = await request.form()
    return [form.get("name"), form.getlist("age")]

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

當然我們也可以通過其它方式:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI, Form
import uvicorn

app = FastAPI()

@app.post("/user")
async def get_user(username: str = Form(...),
                   password: str = Form(...)):
    return {"username": username, "password": password}

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

像 Form 表單,查詢參數、路徑參數等等,都可以和 Request 對象一起使用,像上面的例子,如果我們多定義一個 request: Request,那么我們仍然可以通過 await request.form() 拿到相關的表單信息。所以如果你覺得某個參數不適合類型注解,那么你可以單獨通過 Request 對象進行解析。

文件上傳

那么問題來了,FastAPI 如何接收用戶的文件上傳呢?首先如果想使用文件上傳功能,那么你必須要安裝一個包 python-multipart,直接 pip install python-multipart 即可。

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI, File, UploadFile
import uvicorn

app = FastAPI()

@app.post("/file1")
async def file1(file: bytes = File(...)):
    return f"文件長度: {len(file)}"

@app.post("/file2")
async def file1(file: UploadFile = File(...)):
    return f"文件名: {file.filename}, 文件大小: {len(await file.read())}"


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

我們看到一個直接獲取字節流,另一個是獲取類似於文件句柄的對象。如果是多個文件上傳要怎么做呢?

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from typing import List
from fastapi import FastAPI, UploadFile, File
import uvicorn

app = FastAPI()

@app.post("/file")
async def file(files: List[UploadFile] = File(...)):
    """指定類型為列表即可"""
    for idx, f in enumerate(files):
        files[idx] = f"文件名: {f.filename}, 文件大小: {len(await f.read())}"
    return files

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

此時我們就實現了 FastAPI 文件上傳,當然文件上傳並不影響我們處理表單,可以自己試一下同時處理文件和表單。

返回靜態資源

下面來看看 FastAPI 如何返回靜態資源,首先我們需要安裝 aiofiles,直接 pip 安裝即可。

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
import uvicorn

app = FastAPI()

# name 參數只是起一個名字,FastAPI 內部使用
app.mount("/static", StaticFiles(directory=r"C:\Users\satori\Desktop\bg"), name="static")

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

瀏覽器輸入:localhost:5555/static/1.png,那么會返回 C:\Users\satori\Desktop\bg 下的 1.png 文件。

錯誤處理

錯誤處理也是一個不可忽視的點,錯誤有很多種,比如:

  • 客戶端沒有足夠的權限執行此操作
  • 客戶端沒有訪問某個資源的權限
  • 客戶端嘗試訪問一個不存在的資源
  • ...

這個時候我們應該將錯誤通知相應的客戶端,這個客戶端可以瀏覽器、代碼程序、IoT 設備等等。

但是就我個人而言,更傾向於使用 Response 對象,將里面的  status_code 設置為 404,然后在返回的 json 中指定錯誤信息。不過 FastAPI 內部也提供了一些異常類:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI, HTTPException
import uvicorn

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id != "foo":
        # 里面還可以傳入 headers 設置響應頭
        raise HTTPException(status_code=404, detail="item 沒有發現")
    return {"item": "bar"}

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

HTTPException 是一個普通的 Python 異常類(繼承了 Exception),它攜帶了 API 的相關信息,既然是異常,那么我們不能 return、而是要 raise。

個人覺得這個不是很常用,至少我本人很少用這種方式返回錯誤,因為它能夠攜帶的信息太少了。

自定義異常

FastAPI 內部提供了一個 HTTPException,但是我們也可以自定義,但是注意:我們自定義完異常之后,還要定義一個 handler,將異常和 handler 綁定在一起,然后引發該異常的時候就會觸發相應的 handler。

from fastapi import FastAPI, Request
from fastapi.responses import ORJSONResponse
import uvicorn

app = FastAPI()


class ASCIIException(Exception):
    """何もしません"""

# 通過裝飾器的方式,將 ASCIIException 和 ascii_exception_handler 綁定在一起
@app.exception_handler(ASCIIException)
async def ascii_exception_handler(request: Request, exc: ASCIIException):
    """當引發 ASCIIException 的時候,會觸發 ascii_exception_handler 的執行
       同時會將 request 和 exception 傳過去"""
    return ORJSONResponse(status_code=404, content={"code": 404, "message": "你必須傳遞 ascii 字符串"})

@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if not item_id.isascii():
        raise ASCIIException
    return {"item": f"get {item_id}"}

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

關於 Request、Response,我們除了可以通過 fastapi 進行導入,還可以通過 starlette 進行導入,因為 fastapi 的路由映射是通過 starlette 來實現的。當然我們直接從 fastapi 中進行導入即可。

自定義 404

當訪問一個不存在的 URL,我們應該提示用戶,比如:您要找到頁面去火星了。

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
from fastapi.exceptions import StarletteHTTPException
import uvicorn

app = FastAPI()

@app.exception_handler(StarletteHTTPException)
async def not_found(request, exc):
    return ORJSONResponse({"code": 404, "message": "您要找的頁面去火星了。。。"})

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

此時當我們訪問一個不存在的 URL 時,就會返回我們自定義的 JSON 字符串。

后台任務

如果一個請求耗時特別久,那么我們可以將其放在后台執行,而 FastAPI 已經幫我們做好了這一步。我們來看一下:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
import time
from fastapi import FastAPI, BackgroundTasks
import uvicorn

app = FastAPI()

def send_email(email: str, message: str = ""):
    """發送郵件,假設耗時三秒"""
    time.sleep(3)
    print(f"三秒之后郵件發送給 {email!r}, 郵件信息: {message!r}")

@app.get("/user/{email}")
async def order(email: str, bg_tasks: BackgroundTasks):
    """這里需要多定義一個參數
       此時任務就被添加到后台,當 Response 對象返回之后觸發"""
    bg_tasks.add_task(send_email, email, message="這是一封郵件")
    # 我們在之前介紹 Response 的時候說過,里面有一個參數 background
    # 所以我們也可以將任務放在那里面
    # 因此我們還可以:
    # return Response(orjson.dumps({"message": "郵件發送成功"}), background=BackgroundTask(send_email, email, message="這是一封郵件"))
    return {"message": "郵件發送成功"}


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

首先請求肯定是成功的:

然后響應的 3 秒,終端會出現如下打印:

所以此時任務是被后台執行了的,注意:任務是在響應返回之后才后台執行。

APIRouter

APIRouter 類似於 Flask 中的藍圖,可以更好的組織大型項目,舉個栗子:

在我當前的工程目錄中有一個 app 目錄和一個 main.py,其中 app 目錄中有一個 app01.py,然后我們看看它們是如何組織的。

# app/app01.py
from fastapi import APIRouter

router = APIRouter(prefix="/router")

# 以后訪問的時候要通過 /router/v1 來訪問
@router.get("/v1")
async def v1():
    return {"message": "hello world"}


# main.py
from fastapi import FastAPI
from app.app01 import router
import uvicorn

app = FastAPI()

# 將 router 注冊到 app 中,相當於 Flask 中的 register_blueprint
app.include_router(router)

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

然后可以在外界通過 /router/v1 的方式來訪問。

中間件

中間件在 web 開發中可以說是非常常見了,說白了中間件就是一個函數或者一個類。在請求進入視圖函數之前,會先經過中間件(被稱為請求中間件),而在中間件里面,我們可以對請求進行一些預處理,或者實現一個攔截器等等;同理當視圖函數返回響應之后,也會經過中間件(被稱為響應中間件),在中間件里面,我們也可以對響應進行一些潤色。

自定義中間件

在 FastAPI 里面也支持像 Flask 一樣自定義中間件,但是 Flask 里面有請求中間件和響應中間件,但是在 FastAPI 里面這兩者合二為一了,我們看一下用法。

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI, Request, Response
import uvicorn
import orjson

app = FastAPI()


@app.get("/")
async def view_func(request: Request):
    return {"name": "古明地覺"}


@app.middleware("http")
async def middleware(request: Request, call_next):
    """
    定義一個協程函數,然后使用 @app.middleware("http") 裝飾,即可得到中間件
    """
    # 請求到來時會先經過這里的中間件
    if request.headers.get("ping", "") != "pong":
        response = Response(content=orjson.dumps({"error": "請求頭中缺少指定字段"}),
                            media_type="application/json",
                            status_code=404)
        # 當請求頭中缺少 "ping": "pong",在中間件這一步就直接返回了,就不會再往下走了
        # 所以此時就相當於實現了一個攔截器
        return response
    # 然后,如果條件滿足,則執行 await call_next(request),關鍵是這里的 call_next
    # 如果該中間件后面還有中間件,那么 call_next 就是下一個中間件;如果沒有,那么 call_next 就是對應的視圖函數
    # 這里顯然是視圖函數,因此執行之后會拿到視圖函數返回的 Response 對象
    # 所以我們看到在 FastAPI 中,請求中間件和響應中間件合在一起了
    response: Response = await call_next(request)
    # 這里我們在設置一個響應頭
    response.headers["status"] = "success"
    return response


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

我們可以測試一下:

測試結果也印證了我們的結論。

內置的中間件

通過自定義中間件,我們可以在不修改視圖函數的情況下,實現功能的擴展。但是除了自定義中間件之外,FastAPI 還提供了很多內置的中間件。

app = FastAPI()

# 要求請求協議必須是 https 或者 wss,如果不是,則自動跳轉
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
app.add_middleware(HTTPSRedirectMiddleware)

# 請求中必須包含 Host 字段,為防止 HTTP 主機報頭攻擊,並且添加中間件的時候,還可以指定一個 allowed_hosts,那么它是干什么的呢?
# 假設我們有服務 a.example.com, b.example.com, c.example.com
# 但我們不希望用戶訪問 c.example.com,就可以像下面這么設置,如果指定為 ["*"],或者不指定 allow_hosts,則表示無限制
from starlette.middleware.trustedhost import TrustedHostMiddleware
app.add_middleware(TrustedHostMiddleware, allowed_hosts=["a.example.com", "b.example.com"])

# 如果用戶的請求頭的 Accept-Encoding 字段包含 gzip,那么 FastAPI 會使用 GZip 算法壓縮
# minimum_size=1000 表示當大小不超過 1000 字節的時候就不壓縮了
from starlette.middleware.gzip import GZipMiddleware
app.add_middleware(GZipMiddleware, minimum_size=1000)

除了這些,還有其它的一些內置的中間件,可以自己查看一下,不過不是很常用。

CORS

CORS 過於重要,我們需要單獨拿出來說。

CORS(跨域資源共享)是指瀏覽器中運行的前端里面擁有和后端通信的 JavaScript 代碼,而前端和后端處於不同源的情況。源:協議(http、https)、域(baidu.com、app.com、localhost)以及端口(80、443、8000),只要有一個不同,那么就是不同源。比如下面都是不同的源:

  • http://localhost
  • https://localhost
  • http://localhost:8080

即使它們都是 localhost,但是它們使用了不同的協議或端口,所以它們是不同的源。假設你的前端運行在 localhost:8080,並且嘗試與 localhost:5555 進行通信;然后瀏覽器會向后端發送一個 HTTP OPTIONS 請求,后端會發送適當的 headers 來對這個源進行授權;所以后端必須有一個 "允許的源" 列表,如果前端對應的源是被允許的,瀏覽器才會允許前端向后端發請求,否則就會出現跨域失敗。

而默認情況下,前后端必須是在同一個源,如果不同源那么前端就會請求失敗。而前后端分離早已成為了主流,因此跨域問題是必須要解決的。

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import uvicorn

app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    # 允許跨域的源列表,例如 ["http://www.example.org"] 等等,["*"] 表示允許任何源
    allow_origins=["*"],
    # 跨域請求是否支持 cookie,默認是 False,如果為 True,allow_origins 必須為具體的源,不可以是 ["*"]
    allow_credentials=False,
    # 允許跨域請求的 HTTP 方法列表,默認是 ["GET"]
    allow_methods=["*"],
    # 允許跨域請求的 HTTP 請求頭列表,默認是 [],可以使用 ["*"] 表示允許所有的請求頭
    # 當然 Accept、Accept-Language、Content-Language 以及 Content-Type 總之被允許的
    allow_headers=["*"],
    # 可以被瀏覽器訪問的響應頭, 默認是 [],一般很少指定
    # expose_headers=["*"]
    # 設定瀏覽器緩存 CORS 響應的最長時間,單位是秒。默認為 600,一般也很少指定
    # max_age=1000
)

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

以上即可解決跨域問題。

高階操作

下面我們看一些 FastAPI 的高階操作,這些操作有的不一定能用上,但用上了確實會方便許多。

其它的響應

返回 json 數據可以是:JSONResponse、UJSONResponse、ORJSONResponse,Content-Type 是 application/json;返回 html 是 HTMLResponse,Content-Type 是 text/html;返回 PlainTextResponse,Content-Type 是 text/plain。但是我們還可以有三種響應,分別是返回重定向、字節流、文件。

重定向

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
import uvicorn

app = FastAPI()


@app.get("/index")
async def index():
    return RedirectResponse("https://www.bilibili.com")


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

頁面中訪問 /index 會跳轉到 bilibili。

字節流

返回字節流需要使用異步生成器的方式:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import uvicorn

app = FastAPI()

async def some_video():
    for i in range(5):
        yield f"video {i} bytes ".encode("utf-8")

@app.get("/index")
async def index():
    return StreamingResponse(some_video())


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

如果有文件對象,那么也是可以直接返回的。

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import uvicorn

app = FastAPI()


@app.get("/index")
async def index():
    return StreamingResponse(open("main.py", encoding="utf-8"))


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

文件

返回文件的話,還可以通過 FileResponse:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
from fastapi.responses import FileResponse
import uvicorn

app = FastAPI()

@app.get("/index")
async def index():
    # filename 如果給出,它將包含在響應的 Content-Disposition 中。
    return FileResponse("main.py", filename="這不是main.py")

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

可以自己發請求測試一下。

HTTP 驗證

如果當用戶訪問某個請求的時候,我們希望其輸入用戶名和密碼來確認身份的話該怎么做呢?

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI, Depends
from fastapi.security import HTTPBasic, HTTPBasicCredentials
import uvicorn

app = FastAPI()

security = HTTPBasic()

@app.get("/index")
async def index(credentials: HTTPBasicCredentials = Depends(security)):
    return {"username": credentials.username, "password": credentials.password}

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

當用戶訪問 /index 的時候,會提示輸入用戶名和密碼:

輸入完畢之后,信息會保存在 credentials,我們可以獲取出來進行驗證。

websocket

然后我們來看看 FastAPI 如何實現 websocket:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI
from fastapi.websockets import WebSocket
import uvicorn

app = FastAPI()

@app.websocket("/ws")
async def ws(websocket: WebSocket):
    await websocket.accept()
    while True:
        # websocket.receive_bytes()
        # websocket.receive_json()
        data = await websocket.receive_text()
        await websocket.send_text(f"收到來自客戶端的回復: {data}")

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

然后我們通過瀏覽器進行通信:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        ws = new WebSocket("ws://localhost:5555/ws");
 
        //如果連接成功, 會打印下面這句話, 否則不會打印
        ws.onopen = function () {
            console.log('連接成功')
        };
 
        //接收數據, 服務端有數據過來, 會執行
        ws.onmessage = function (event) {
            console.log(event)
        };
 
        //服務端主動斷開連接, 會執行.
        //客戶端主動斷開的話, 不執行
        ws.onclose = function () {  }
 
    </script>
</body>
</html>

FastAPI 服務的部署

目前的話,算是介紹了 FastAPI 的絕大部分內容,然后我們來看看 FastAPI 服務的部署,其實部署很簡單,直接 uvicorn.run 即可。但是這里面有很多的參數,我們主要是想要介紹這些參數。

def run(app, **kwargs):
    config = Config(app, **kwargs)
    server = Server(config=config)
    ...
    ...

我們看到 app 和 **kwargs 都傳遞給了 Config,所以我們只需要看 Config 里面都有哪些參數即可。這里選出一部分:

  • app:第一個參數,不需要解釋
  • host:監聽的ip
  • port:監聽的端口
  • uds:綁定的 unix domain socket,一般不用
  • fd:從指定的文件描述符中綁定 socket
  • loop:事件循環實現,可選項為 auto|asyncio|uvloop|iocp
  • http:HTTP 協議實現,可選項為 auto|h11|httptools
  • ws:websocket 協議實現,可選項為 auto|none|websockets|wsproto
  • lifespan:lifespan 實現,可選項為 auto|on|off
  • env_file:環境變量配置文件
  • log_config:日志配置文件
  • log_level:日志等級
  • access_log:是否記錄日志
  • use_colors:是否帶顏色輸出日志信息
  • interface:應用接口,可選 auto|asgi3|asgi2|wsgi
  • debug:是否開啟 debug 模式
  • reload:是否自動重啟
  • reload_dirs:要自動重啟的目錄
  • reload_delay:多少秒后自動重啟
  • workers:工作進程數
  • limit_concurrency:並發的最大數量
  • limit_max_requests:能 hold 住的最大請求數

小結

總的來說,FastAPI 算是當前最流行的異步框架了,並且它完全可以在生產中使用,是值得信賴的。當然使用異步框架,最重要的是要搭配一個異步驅動去訪問數據庫,因為 web 服務的瓶頸都是在數據庫上面。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM