* 🌐 Update translations for ru (update-outdated) * 🎨 Auto format * Apply suggestions from code review * Apply suggestions from code review 2 * Apply suggestions from code review 3 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com>
11 KiB
Дополнительные модели
В продолжение прошлого примера будет уже обычным делом иметь несколько связанных между собой моделей.
Это особенно применимо в случае моделей пользователя, потому что:
- Модель для ввода должна иметь возможность содержать пароль.
- Модель для вывода не должна содержать пароль.
- Модель для базы данных, возможно, должна содержать хэшированный пароль.
/// danger | Внимание
Никогда не храните пароли пользователей в чистом виде. Всегда храните "безопасный хэш", который вы затем сможете проверить.
Если вам это не знакомо, вы можете узнать про "хэш пароля" в главах о безопасности{.internal-link target=_blank}.
///
Множественные модели
Ниже изложена основная идея того, как могут выглядеть эти модели с полями для паролей, а также описаны места, где они используются:
{* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *}
Про **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,
)
/// warning | Предупреждение
Вспомогательные дополнительные функции fake_password_hasher и fake_save_user используются только для демонстрации возможного потока данных и, конечно, не обеспечивают настоящую безопасность.
///
Сократите дублирование
Сокращение дублирования кода — это одна из главных идей FastAPI.
Поскольку дублирование кода повышает риск появления багов, проблем с безопасностью, проблем десинхронизации кода (когда вы обновляете код в одном месте, но не обновляете в другом), и т.д.
А все описанные выше модели используют много общих данных и дублируют названия атрибутов и типов.
Мы можем это улучшить.
Мы можем определить модель UserBase, которая будет базовой для остальных моделей. И затем мы можем создать подклассы этой модели, которые будут наследовать её атрибуты (объявления типов, валидацию, и т.п.).
Все операции конвертации, валидации, документации, и т.п. будут по-прежнему работать нормально.
В этом случае мы можем определить только различия между моделями (с password в чистом виде, с hashed_password и без пароля):
{* ../../docs_src/extra_models/tutorial002_py310.py hl[7,13:14,17:18,21:22] *}
Union или anyOf
Вы можете объявить HTTP-ответ как Union из двух или более типов. Это означает, что HTTP-ответ может быть любым из них.
Он будет определён в OpenAPI как anyOf.
Для этого используйте стандартную аннотацию типов в Python typing.Union:
/// note | Примечание
При объявлении Union сначала указывайте наиболее специфичный тип, затем менее специфичный. В примере ниже более специфичный PlaneItem стоит перед CarItem в Union[PlaneItem, CarItem].
///
{* ../../docs_src/extra_models/tutorial003_py310.py hl[1,14:15,18:20,33] *}
Union в Python 3.10
В этом примере мы передаём Union[PlaneItem, CarItem] в качестве значения аргумента response_model.
Поскольку мы передаём его как значение аргумента вместо того, чтобы поместить его в аннотацию типа, нам придётся использовать Union даже в Python 3.10.
Если оно было бы указано в аннотации типа, то мы могли бы использовать вертикальную черту как в примере:
some_variable: PlaneItem | CarItem
Но если мы поместим это в присваивание response_model=PlaneItem | CarItem, мы получим ошибку, потому что Python попытается произвести некорректную операцию между PlaneItem и CarItem вместо того, чтобы интерпретировать это как аннотацию типа.
Список моделей
Таким же образом вы можете объявлять HTTP-ответы, возвращающие списки объектов.
Для этого используйте стандартный typing.List в Python (или просто list в Python 3.9 и выше):
{* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *}
Ответ с произвольным dict
Вы также можете объявить HTTP-ответ, используя обычный произвольный dict, объявив только тип ключей и значений, без использования Pydantic-модели.
Это полезно, если вы заранее не знаете корректных названий полей/атрибутов (которые будут нужны при использовании Pydantic-модели).
В этом случае вы можете использовать typing.Dict (или просто dict в Python 3.9 и выше):
{* ../../docs_src/extra_models/tutorial005_py39.py hl[6] *}
Резюме
Используйте несколько Pydantic-моделей и свободно применяйте наследование для каждого случая.
Вам не обязательно иметь единственную модель данных для каждой сущности, если эта сущность должна иметь возможность быть в разных "состояниях". Как в случае с "сущностью" пользователя, у которого есть состояние, включающее password, password_hash и отсутствие пароля.