Дополнительные модели¶
В продолжение прошлого примера, будет обычным делом иметь несколько связанных между собой моделей.
Это особенно касается моделей пользователя, потому что:
- Модель для ввода должна иметь возможность содержать пароль.
- Модель для вывода не должна содержать пароль.
- Модель для базы данных, вероятно, должна содержать хэшированный пароль.
Внимание
Никогда не храните пароли пользователей в чистом виде. Всегда храните "защищенный хэш", который вы затем сможете проверять.
Если вы не знаете, вы узнаете, что такое "хэш пароля" в главах о безопасности.
Множественные модели¶
Ниже представлено общее представление о том, как могут выглядеть эти модели с их полями для паролей и где они используются:
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.dict(), 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
🤓 Other versions and variants
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: Union[str, None] = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: Union[str, None] = None
class UserInDB(BaseModel):
username: str
hashed_password: str
email: EmailStr
full_name: Union[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.dict(), 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
Информация
В Pydantic v1 метод назывался .dict(), в Pydantic v2 он устарел (но все еще поддерживается) и был переименован в .model_dump().
В приведенных примерах используется .dict() для совместимости с Pydantic v1, но вам следует использовать .model_dump(), если вы можете использовать Pydantic v2.
О **user_in.dict()¶
.dict() из Pydantic¶
user_in — это Pydantic модель класса UserIn.
У Pydantic моделей есть метод .dict(), который возвращает dict с данными модели.
Поэтому, если мы создадим Pydantic объект user_in таким образом:
user_in = UserIn(username="john", password="secret", email="[email protected]")
и затем вызовем:
user_dict = user_in.dict()
то теперь у нас есть dict с данными в переменной user_dict (это dict вместо объекта Pydantic модели).
И если мы вызовем:
print(user_dict)
мы бы получили Python dict с:
{
'username': 'john',
'password': 'secret',
'email': '[email protected]',
'full_name': None,
}
Распаковка dict¶
Если мы возьмем dict, аналогичный user_dict, и передадим его в функцию (или класс) с **user_dict, Python его "распакует". Он передаст ключи и значения user_dict напрямую как аргументы типа ключ-значение.
Поэтому, продолжая с user_dict, написание следующего кода:
UserInDB(**user_dict)
будет аналогично следующему:
UserInDB(
username="john",
password="secret",
email="[email protected]",
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.dict(), этот код:
user_dict = user_in.dict()
UserInDB(**user_dict)
будет равнозначен следующему:
UserInDB(**user_in.dict())
...потому что user_in.dict() — это dict, и затем мы заставляем Python его "распаковать" при передаче в UserInDB, поставив впереди **.
Таким образом, мы получаем Pydantic модель на основе данных из другой Pydantic модели.
Распаковка dict и дополнительные именованные аргументы¶
И затем добавляя дополнительный именованный аргумент hashed_password=hashed_password, как в:
UserInDB(**user_in.dict(), 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.dict(), 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
🤓 Other versions and variants
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserBase(BaseModel):
username: str
email: EmailStr
full_name: Union[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.dict(), 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 typing import Union
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=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
return items[item_id]
🤓 Other versions and variants
from typing import Union
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=Union[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 вместо того, чтобы интерпретировать это как аннотацию типа.
Список моделей¶
Аналогично, вы можете объявить ответы в виде списков объектов.
Для этого используйте стандартный typing.List из Python (или просто list в Python 3.9 и выше):
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
🤓 Other versions and variants
from typing import 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 модели).
В этом случае можно использовать typing.Dict (или просто dict в Python 3.9 и выше):
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}
🤓 Other versions and variants
from typing import 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 и без пароля.