Skip to content

Body - 巢狀模型

🌐 AI 與人類共同完成的翻譯

此翻譯由人類指導的 AI 完成。🤝

可能會有對原意的誤解,或讀起來不自然等問題。🤖

你可以透過協助我們更好地引導 AI LLM來改進此翻譯。

英文版

使用 FastAPI,你可以定義、驗證、文件化,並使用任意深度的巢狀模型(感謝 Pydantic)。

列表欄位

你可以將屬性定義為某個子型別。例如,Python 的 list

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list = []


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

這會讓 tags 成為一個列表,儘管尚未宣告列表元素的型別。

具有型別參數的列表欄位

不過,Python 有一種專門的方式來宣告具有內部型別(「型別參數」)的列表:

宣告帶有型別參數的 list

要宣告具有型別參數(內部型別)的型別,例如 listdicttuple,使用方括號 [] 傳入內部型別作為「型別參數」:

my_list: list[str]

以上都是標準的 Python 型別宣告語法。

對於具有內部型別的模型屬性,也使用相同的標準語法。

因此,在我們的範例中,可以讓 tags 明確成為「字串的列表」:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list[str] = []


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

集合型別

但進一步思考後,我們會意識到 tags 不應該重覆,應該是唯一的字串。

而 Python 有一種用於唯一元素集合的特殊資料型別:set

因此我們可以將 tags 宣告為字串的 set

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

這樣一來,即使收到包含重覆資料的請求,也會被轉換為由唯一元素組成的 set

之後只要輸出該資料,即使來源有重覆,也會以唯一元素的 set 輸出。

並且也會在註解/文件中相應標示。

巢狀模型

每個 Pydantic 模型的屬性都有型別。

而該型別本身也可以是另一個 Pydantic 模型。

因此,你可以宣告具有特定屬性名稱、型別與驗證的深度巢狀 JSON「物件」。

而且可以任意深度巢狀。

定義子模型

例如,我們可以定義一個 Image 模型:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Image(BaseModel):
    url: str
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    image: Image | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

將子模型作為型別使用

然後把它作為某個屬性的型別使用:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Image(BaseModel):
    url: str
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    image: Image | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

這表示 FastAPI 會期望一個類似如下的本文:

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2,
    "tags": ["rock", "metal", "bar"],
    "image": {
        "url": "http://example.com/baz.jpg",
        "name": "The Foo live"
    }
}

只需進行上述宣告,使用 FastAPI 你就能獲得:

  • 編輯器支援(自動完成等),即使是巢狀模型
  • 資料轉換
  • 資料驗證
  • 自動產生文件

特殊型別與驗證

除了 strintfloat 等一般的單一型別外,你也可以使用繼承自 str 的更複雜單一型別。

若要查看所有可用選項,請參閱 Pydantic 的型別總覽。你會在下一章看到一些範例。

例如,在 Image 模型中有一個 url 欄位,我們可以將其宣告為 Pydantic 的 HttpUrl,而不是 str

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    image: Image | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

該字串會被檢查是否為有效的 URL,並在 JSON Schema / OpenAPI 中相應註記。

具有子模型列表的屬性

你也可以將 Pydantic 模型作為 listset 等的子型別使用:

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    images: list[Image] | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

這會期望(並進行轉換、驗證、文件化等)如下的 JSON 本文:

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2,
    "tags": [
        "rock",
        "metal",
        "bar"
    ],
    "images": [
        {
            "url": "http://example.com/baz.jpg",
            "name": "The Foo live"
        },
        {
            "url": "http://example.com/dave.jpg",
            "name": "The Baz"
        }
    ]
}

Info

注意 images 鍵現在是一個由 image 物件組成的列表。

深度巢狀模型

你可以定義任意深度的巢狀模型:

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    images: list[Image] | None = None


class Offer(BaseModel):
    name: str
    description: str | None = None
    price: float
    items: list[Item]


@app.post("/offers/")
async def create_offer(offer: Offer):
    return offer

Info

請注意,Offer 具有一個 Item 的列表,而每個 Item 又有一個可選的 Image 列表。

純列表的本文

如果你期望的 JSON 本文頂層值是一個 JSON array(Python 的 list),可以像在 Pydantic 模型中那樣,直接在函式參數上宣告其型別:

images: list[Image]

如下所示:

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


@app.post("/images/multiple/")
async def create_multiple_images(images: list[Image]):
    return images
🤓 Other versions and variants
from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


@app.post("/images/multiple/")
async def create_multiple_images(images: list[Image]):
    return images

隨處可得的編輯器支援

你將在各處獲得編輯器支援。

即使是列表中的項目也一樣:

若直接操作 dict 而不是使用 Pydantic 模型,就無法獲得這種等級的編輯器支援。

但你也不必擔心,傳入的 dict 會自動被轉換,而你的輸出也會自動轉換為 JSON。

任意 dict 的本文

你也可以將本文宣告為一個 dict,其鍵為某種型別、值為另一種型別。

如此一來,你無需事先知道有效的欄位/屬性名稱為何(不像使用 Pydantic 模型時需要)。

這在你想接收尚未預知的鍵時很有用。


另一個實用情境是當你希望鍵是其他型別(例如,int)時。

這正是我們在此要示範的。

在此情況下,只要是擁有 int 鍵且對應 float 值的 dict 都會被接受:

from fastapi import FastAPI

app = FastAPI()


@app.post("/index-weights/")
async def create_index_weights(weights: dict[int, float]):
    return weights
🤓 Other versions and variants
from fastapi import FastAPI

app = FastAPI()


@app.post("/index-weights/")
async def create_index_weights(weights: dict[int, float]):
    return weights

Tip

請記住,JSON 只支援 str 作為鍵。

但 Pydantic 具有自動資料轉換。

這表示即使你的 API 用戶端只能以字串作為鍵,只要這些字串是純整數,Pydantic 會自動轉換並驗證它們。

而你作為 weights 所接收的 dict,實際上會擁有 int 鍵與 float 值。

總結

使用 FastAPI,你在保持程式碼簡潔優雅的同時,亦可擁有 Pydantic 模型所提供的高度彈性。

同時還具備以下優點:

  • 編輯器支援(到處都有自動完成!)
  • 資料轉換(亦即 parsing/serialization)
  • 資料驗證
  • 結構描述(Schema)文件
  • 自動產生文件