Перейти до змісту

Додаткові моделі

🌐 Переклад ШІ та людьми

Цей переклад виконано ШІ під керівництвом людей. 🤝

Можливі помилки через неправильне розуміння початкового змісту або неприродні формулювання тощо. 🤖

Ви можете покращити цей переклад, допомігши нам краще спрямовувати AI LLM.

Англійська версія

Продовжуючи попередній приклад, часто потрібно мати більше ніж одну пов’язану модель.

Особливо це стосується моделей користувача, тому що:

  • вхідна модель повинна мати пароль.
  • вихідна модель не повинна містити пароль.
  • модель бази даних, ймовірно, повинна містити хеш пароля.

Обережно

Ніколи не зберігайте паролі користувачів у відкритому вигляді. Завжди зберігайте «безпечний хеш», який потім можна перевірити.

Якщо ви ще не знаєте, що таке «хеш пароля», ви дізнаєтесь у розділах про безпеку.

Кілька моделей

Ось загальна ідея того, як можуть виглядати моделі з їхніми полями пароля та місцями використання:

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


class UserInDB(BaseModel):
    username: str
    hashed_password: str
    email: EmailStr
    full_name: str | None = None


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved

Про **user_in.model_dump()

.model_dump() у Pydantic

user_in - це модель Pydantic класу UserIn.

Моделі Pydantic мають метод .model_dump(), який повертає dict з даними моделі.

Отже, якщо ми створимо об’єкт Pydantic user_in так:

user_in = UserIn(username="john", password="secret", email="john.doe@example.com")

і викличемо:

user_dict = user_in.model_dump()

тепер ми маємо dict з даними у змінній user_dict (це dict, а не об’єкт моделі Pydantic).

А якщо викликати:

print(user_dict)

ми отримаємо Python dict з:

{
    'username': 'john',
    'password': 'secret',
    'email': 'john.doe@example.com',
    'full_name': None,
}

Розпакування dict

Якщо взяти dict, наприклад user_dict, і передати його у функцію (або клас) як **user_dict, Python «розпакує» його. Ключі та значення user_dict будуть передані безпосередньо як іменовані аргументи.

Отже, продовжуючи з user_dict вище, запис:

UserInDB(**user_dict)

дасть еквівалентний результат:

UserInDB(
    username="john",
    password="secret",
    email="john.doe@example.com",
    full_name=None,
)

А точніше, використовуючи безпосередньо user_dict, з будь-яким його вмістом у майбутньому:

UserInDB(
    username = user_dict["username"],
    password = user_dict["password"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
)

Модель Pydantic зі вмісту іншої

Як у прикладі вище ми отримали user_dict з user_in.model_dump(), цей код:

user_dict = user_in.model_dump()
UserInDB(**user_dict)

буде еквівалентним:

UserInDB(**user_in.model_dump())

...тому що user_in.model_dump() повертає dict, а ми змушуємо Python «розпакувати» його, передаючи в UserInDB з префіксом **.

Тож ми отримуємо модель Pydantic з даних іншої моделі Pydantic.

Розпакування dict і додаткові ключові аргументи

Додаючи додатковий іменований аргумент hashed_password=hashed_password, як тут:

UserInDB(**user_in.model_dump(), hashed_password=hashed_password)

...у підсумку це дорівнює:

UserInDB(
    username = user_dict["username"],
    password = user_dict["password"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
    hashed_password = hashed_password,
)

Попередження

Додаткові допоміжні функції fake_password_hasher і fake_save_user лише демонструють можливий потік даних і, звісно, не забезпечують реальної безпеки.

Зменшення дублювання

Зменшення дублювання коду - одна з ключових ідей у FastAPI.

Адже дублювання коду підвищує ймовірність помилок, проблем безпеки, розсинхронізації коду (коли ви оновлюєте в одному місці, але не в інших) тощо.

І ці моделі спільно використовують багато даних та дублюють назви і типи атрибутів.

Можна зробити краще.

Можна оголосити модель UserBase, яка буде базовою для інших моделей. Потім створити підкласи цієї моделі, що наслідуватимуть її атрибути (оголошення типів, валідацію тощо).

Уся конвертація даних, валідація, документація тощо працюватимуть як зазвичай.

Таким чином, ми оголошуємо лише відмінності між моделями (з відкритим password, з hashed_password і без пароля):

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserBase(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


class UserIn(UserBase):
    password: str


class UserOut(UserBase):
    pass


class UserInDB(UserBase):
    hashed_password: str


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved

Union або anyOf

Ви можете оголосити відповідь як Union двох або більше типів - це означає, що відповідь може бути будь-якого з них.

В OpenAPI це буде визначено як anyOf.

Для цього використайте стандартну підказку типу Python typing.Union:

Примітка

Під час визначення Union спочатку вказуйте найконкретніший тип, а потім менш конкретний. У прикладі нижче більш конкретний PlaneItem стоїть перед CarItem у Union[PlaneItem, CarItem].

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class BaseItem(BaseModel):
    description: str
    type: str


class CarItem(BaseItem):
    type: str = "car"


class PlaneItem(BaseItem):
    type: str = "plane"
    size: int


items = {
    "item1": {"description": "All my friends drive a low rider", "type": "car"},
    "item2": {
        "description": "Music is my aeroplane, it's my aeroplane",
        "type": "plane",
        "size": 5,
    },
}


@app.get("/items/{item_id}", response_model=PlaneItem | CarItem)
async def read_item(item_id: str):
    return items[item_id]

Union у Python 3.10

У цьому прикладі ми передаємо Union[PlaneItem, CarItem] як значення аргументу response_model.

Оскільки ми передаємо його як значення аргументу, а не в анотації типу, потрібно використовувати Union навіть у Python 3.10.

Якби це була анотація типу, можна було б використати вертикальну риску, наприклад:

some_variable: PlaneItem | CarItem

Але якщо записати це як присвоєння response_model=PlaneItem | CarItem, отримаємо помилку, тому що Python спробує виконати невалідну операцію між PlaneItem і CarItem, замість того щоб трактувати це як анотацію типу.

Список моделей

Аналогічно можна оголошувати відповіді як списки об’єктів.

Для цього використайте стандартний Python list:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str


items = [
    {"name": "Foo", "description": "There comes my hero"},
    {"name": "Red", "description": "It's my aeroplane"},
]


@app.get("/items/", response_model=list[Item])
async def read_items():
    return items

Відповідь з довільним dict

Також можна оголосити відповідь, використовуючи звичайний довільний dict, вказавши лише типи ключів і значень, без моделі Pydantic.

Це корисно, якщо ви заздалегідь не знаєте допустимі назви полів/атрибутів (які були б потрібні для моделі Pydantic).

У такому разі можна використати dict:

from fastapi import FastAPI

app = FastAPI()


@app.get("/keyword-weights/", response_model=dict[str, float])
async def read_keyword_weights():
    return {"foo": 2.3, "bar": 3.4}

Підсумок

Використовуйте кілька моделей Pydantic і вільно наслідуйте для кожного випадку.

Не обов’язково мати одну модель даних на сутність, якщо ця сутність може мати різні «стани». Як у випадку сутності користувача зі станами: з password, з password_hash і без пароля.