Додаткові моделі¶
🌐 Переклад ШІ та людьми
Цей переклад виконано ШІ під керівництвом людей. 🤝
Можливі помилки через неправильне розуміння початкового змісту або неприродні формулювання тощо. 🤖
Ви можете покращити цей переклад, допомігши нам краще спрямовувати 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 і без пароля.