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] *}
/// info | Информация
В 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="john.doe@example.com")
и затем вызовем:
user_dict = user_in.dict()
то теперь у нас есть 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.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,
)
/// 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
Вы можете объявить, что ответ должен быть Union
из двух или более типов, это значит, что ответ может соответствовать любому из них.
Это будет определено в 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
вместо того, чтобы интерпретировать это как аннотацию типа.
Список моделей
Аналогично, вы можете объявить ответы в виде списков объектов.
Для этого используйте стандартный typing.List
из Python (или просто list
в Python 3.9 и выше):
{* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *}
Ответ с произвольным dict
Вы также можете объявить ответ, используя произвольный одноуровневый dict
, задавая только типы ключей и значений без использования Pydantic модели.
Это полезно, если вы не знаете заранее допустимые названия полей/атрибутов (которые понадобятся при использовании Pydantic модели).
В этом случае можно использовать typing.Dict
(или просто dict
в Python 3.9 и выше):
{* ../../docs_src/extra_models/tutorial005_py39.py hl[6] *}
Резюме
Используйте несколько Pydantic моделей и свободно применяйте наследование в каждом случае.
Вам не обязательно иметь единственную модель данных для каждой сущности, если эта сущность должна иметь возможность быть в разных "состояниях". Как в случае с "сущностью" пользователя, у которого есть состояния с полями password
, password_hash
и без пароля.