You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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 и без пароля.