Не суйте свой Pydantic в мое Django

от автора

image

Было замечательное теплое австрийское утро, и ничего не предвещало … ничего, пока мой коллега не порекомендовал мне посмотреть запись недавно прошедшей Pyconf.

Там кто-то рассказывал, как при помощи желтого скотча, такой-то матери и усилий любимых разработчиков они наконец-то допилили Django Rest Framework до состояния франкенштейна подходящего его компании. Презентация выглядела странно, может я и прошел бы мимо, но моменты упоминания докладчиком PYDANTIC вызвали у меня явные сомнения в нормальности происходящего.

Оставим получившегося фRESTенштейна для другой статьи, и поразмышляем только о прозвучавшей в докладе возможности использования PYDANTIC в экосистеме Django — DRF.

Предпосылка

В компании «Плохой больной» используется экосистема DJANGO-DRF-API. Компанию Django-Rest-Framework не устраивал и был переписан. Тимлидера разработчиков это не останавило, он слышал что-то хорошее про PYDANTIC и твердо решил внедрить его в DJANGO проект. Пока безуспешно.

Введение

PYDANTIC — это модуль python, позволяющий объявить специальный класс PYTHON, в котором атрибуты класса имеют статическую типизацию. Эта типизация используется в момент создания объекта класса для проверки значений, присваиваемых этим атрибутам.

Допустим, в Django проекте есть модель

class Organization(models.Model):     domain = models.CharField(_('Domain'), max_length=25, unique=True, validators=(DomainValidator(),))

напомню, что id у такой модели создастся автоматически.

Если на базе Django-модели Organization создать аналогичный PYDANTIC-класс, то объект этого класса можно использовать в роли обработчика входящих «сырых» данных для последующего безопасного использования. Кто-то даже утверждает, что PYDANTIC-классы кроме валидации данных якобы можно использовать для сериализации данных нет.

Пример объявления PYDANTIC-класса

from pydantic import BaseModel class OrganizationSchema(BaseModel):     id: int     domain: str     class Config:         min_anystr_length = 1         max_anystr_length = 25

Я добавил настройку валидации для строковых данных.

Увы нормальное создание на лету PYDANTIC-классов на базе Django-моделей невозможно. Это минус. Решение возможно, но есть нюанс.

Вариант 1. С нюансом

Можно взять недоделанный модуль djantic, он обещает создать PYDANTIC-класс на базе Django-модели. Выглядит это так:

from djantic.main import ModelSchema class OrganizationSchema(ModelSchema):     class Config:         model = Organization         include = ['id', 'domain']

Нюанс в том, что не работает никак.

Можно, конечно, ручками доделать все, что не доделал автор, типа навешивания валидаторов:

    @validator('domain')     def domainvalidator(cls, v):         DomainValidator(v)         DomainUniqueValidator(v)         return v

Можно даже сделать автоматическое прикрепление валидаторов к OrganizationSchema в цикле:

for field in (Organisation._meta.fields('domain'), Organisation._meta.fields('id')):     setattr(OrganizationSchema, f’validate_{field.name}’, validator(field.name)(lambda cls,value: not all(validator(value) for validator in field.validators) and value) 

 
После некоторых доделок djantic у меня все же начал валидировать.

Из-за сырости пакета, не рекомендую его использовать – вы потратите те же усилия для достижения результата, что и с обычным PYDANTIC.

Вариант 2. Нюансов не меньше

Берем PYDANTIC-класс, объявленный выше. Он не связан с Django моделью, не знает, как нормально валидировать данные и не умеет чистить результаты. Вместо этого в PYDANTIC-классе можно объявить валидатор поля, в котором предлагается все это делать. В Django за это отвечают методы to_python, validate, и clean полей модели.

Мне не нравится, когда смешиваются разнородные идеологии внутри одного проекта, потому субъективный минус.

Nested PYDANTIC-класс тоже возможен:

class OrganizationsList(BaseModel):     __root__: List[OrganizationSchema]

Применение. Без нюансов

Не важно, какой вариант выбран, пробуем реализовать следующее утверждение:

Объекты PYDANTIC возможно использовать в DJANGO и в DRF вместо объектов Django-form и DRF-serialiser соответственно.

Увы, без мега напильника сделать это не получится, но, надеюсь, мы все же найдем ответ на вопрос: КАКОЙ В ЭТОМ СМЫСЛ?

Создаем DRF-API на базе ListAPIView, этот обработчик будет выдавать лист объектов модели Organization из базы, добавим в него метод POST для сериализации и валидации данных, отправляемых пользователем:

class OrganisationUpdateApiView(ListAPIView):     http_method_names = ['get', 'post']  # , 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'     serializer_class = OrganisationSerializer     permission_classes = (AllowAny,)     queryset = Organization.objects.only('id', 'domain')      def post(self, *args, **kwargs):         serializer = self.get_serializer(None, data=self.request.data)         serializer.is_valid(raise_exception=True)          return Response(serializer.validated_data)

результат работы подобной API

b'[{"id": 1, "domain": "localhost"}]'  # тело ответа на GET запрос [{'id': 1, 'domain': 'localhost'}]  # validated_data после POST запроса

Стандартный DRF сериализатор для API:

class OrganisationSerializer(ModelSerializer):     class Meta:         model = Organization         fields = ['id', 'domain']         extra_kwargs = {'id': {'read_only': False}, 'domain': {'validators': []}}

Я убрал валидаторы домена, для соответствия результатам работы следующего сериализатора на базе PYDANTIC-класса:

class PydantedOrganisationSerializers:     def __init__(self, queryset=None, **kwargs):         super(PydantedOrganisationSerializers, self).__init__()         vars(self).update(queryset=queryset, kwargs=kwargs)      @property     def validated_data(self):         pydanted = OrganizationSchema         try:             data = OrganizationsList(__root__=(pydanted(**kwargs) for kwargs in self.queryset.values('id', 'domain')))         except Exception as error:             data = error         return data.json()      def is_valid():         return True

Кстати, если мы посмотрим на PYDANTIC, то он умеет напрямую разбирать JSON строку и собирать обратно собственными методами parse_raw()/json(). Это плюс. Потому при встраивании PYDANTIC сериализатора в DRF-API стоит отключить классы JSONRenderer и JSONParser, запускаемые по умолчанию DRF-View, и использовать соответствующие методы PYDANTIC-Модели.

class PydantedOrganisationUpdateApiView(OrganisationUpdateApiView):     renderer_classes = []     parser_classes = []

На ресурсах типа SOf в обсуждениях про использование PYDANTIC в DRF я не встречал упоминаний использования встроенных методов parse_raw/json.

Я уже хотел запустить проект, но тут подумалось, что если уж я сравниваю результаты, то эксперимент будет не полным без результатов работы сериализатора, построенного на базе Django-Form:

#  создаю модельную форму class MyModelForm(forms.ModelForm):     class Meta:         fields = ['id', 'domain']         model = Organization     id = forms.IntegerField(label=_('auto key'), min_value=0, required=True)     domain = forms.CharField(label=_('Domain'), max_length=25, required=True)      def validate_unique(self):         return #  А теперь создаю сериализатор class OrmSerializer(object):     def __init__(self, *args, **kwargs):         self._data = kwargs.get('data') or {}      def is_valid(self, *args, **kwargs):         forms = (MyModelForm(data) for data in self._data)         self.validated_data = [form.cleaned_data if form.is_valid() else form.errors for form in forms]         return True

Я уже хотел запустить проект, но меня было уже не остановить: я вспомнил еще один «сериализатор» из Django! Это же объект класса Model с его встроенными методами Model.full_clean и Model.serialize_value:

class DjangoModelSerializer:     exclude = set(field.name for field in Organization._meta.fields if field.name not in ('id', 'domain'))      def __init__(self, *args, **kwargs):         self._data = kwargs.get('data') or {}      def is_valid(self, *args, **kwargs):          def data_yielder(full_data):             for _data in full_data:                 try:                     Organization(**_data).full_clean(exclude=self.exclude, validate_unique=False)                 except ValidationError as errors:                     _data = errors                 yield _data         self.data = self.validated_data = [data for data in data_yielder(self._data)]         return True  # конечно же тут надо возвращать False если была ошибка

Если добравшийся до этого момента читатель спросит: это все? Я отвечу, что есть еще варианты сериализаторов. Каждый из вас может добавить свой вариант в код репозитория.

Следующие шаги:

  1. Создаем несколько объектов Django-модели Organisation
  2. Получаем сериализованный лист объектов из базы по GET-запросу
  3. Отправляем его обратно POST-запросом
  4. Получаем набор сериализованных объектов
  5. Сравниваем время работы всех объявленых сериализаторов.
  6. Профит

В репозитории в папке TEST вы найдете файл, который выполняет все действия, команды запуска в readme. Можно настроить количество сериализуемых объектов, печать в консоль и добавление ошибок в объекты.

Момент истины

GET POST без ошибок
Создавалось 3,30,300,3000 и 30000  + 1 объектов. Графики в логарифмическом масштабе. Тот, кто ниже всех — выиграл.

GET

Безоговорочным победителем сериализации объектов из базы в строку JSON стал ORM.VALUES_LIST + DRF JSONRenderer. 0.18sec/30000 объектов. Не секрет, что в некоторых gresSQL можно сделать еще быстрее.

Меня удивил DRF-сериализатор без доп настроек (самая верхняя линия). Он работает на сериализацию объектов из базы в 100 раз медленнее — 18,5sec/30000 объектов.

Остальные сериализаторы стоят вместе, практически с одинаковыми результатами.

Сравнение результатов, не совсем адекватно: REST сначала создает объекты и потом их сериализует, чего «победитель» не делает. Именно это я имел ввиду, когда говорил «без доп настроек». Если вы хотите увидеть честный результат, OrganisationSerializer надо доработать.

POST

Самым медленным оказался сериализатор на базе DJANGO FORM, в 5 раз медленнее остальных.
DRF-сериализатор без доп настроек работает тоже медленно, в 2 раза медленнее остальных двух.

А вот победителем, как мне кажется, оказался сериализатор на базе models.MODEL У меня он был быстрее в 3х случаях из 5, чем PYDANTIC сериализатор. Предлагаю это проверить читателям самостоятельно.

Сравнение результатов сериализации не совсем адекватно: PYDANTIC выдает сериализованные объекты своего класса, для использования далее в DAJNGO их скорее всего придется преобразовывать в объекты Dajngo.

Работа с поврежденными объектами

Мне не удалось заставить работать PYDANTIC сериализатор, в случае всего одного поврежденного объекта из нескольких. И на GET и на POST результат был «[{‘loc’: (‘__root__’, 0, ‘domain’), ‘msg’: ‘field required’, ‘type’: ‘value_error.missing’}]» Если вы знаете, как это можно исправить, жду помощи в комментариях. PYDANTIC пока выбывает из участия в этом тесте.

GET POST с ошибками
Конечно, это так себе тест: я получаю из базы уже «поврежденный» объект. Допустим, что такое в реальности тоже возможно. 🙂

GET

Безоговорочным победителем сериализации объектов из базы в строку JSON стал ORM.VALUES_LIST + DRF JSONRenderer.

Чистый DRF-сериализатор без доп настроек на последнем месте.

POST

Родной DRF-сериализатор без доп настроек работает наравне с сериализатором на базе models.MODEL, хотя последний все же быстрее.

Каждый сериализатор сообщает об ошибке по-своему, мне нравится, когда в листе объектов видно, какой объект не прошел валидацию:

[{'domain': ['This field is required.']}, {'domain': 'GkByUSnIFVRrcA7WFAAonMjeu', 'id': 2},…]

Пример ошибки валидации самого медленного сериализатора на базе DJANGO Form.

Кстати, все результаты возможно убыстрить, если вместо билиотеки JSON использовать UJSON.

Итоги

Итоги оказались для меня неожиданными.

Who Ser-OUT Ser-IN no Err Ser-IN +Err ErrMessage From BOX Easyness Django-ECO MultiLang  
Dj Form WerySlow WerySlow WerySlow Normal Yes Easy yes Yes 3
Dj Model Fast Normal Normal Normal Yes Easy Yes Yes 1
DRF Slow Normal Normal Best No Normal Yes Yes 2
PYDANTIC Fast Normal Bad No Difficult No No 4
ORM Fastest Yes Easy Yes Yes 3

Это очень субъективная таблица. Мне, например, важно уметь перевести сообщение об ошибке, а в PYDANTIC это не реализовано, или, «ИЗ КОРОБКИ» сериализатор DRF может только простые вещи, иначе надо настраивать. И т.п. А кому-то это, может быть, не важно.

Так какие же у нас выводы?

  • Существующее решение на базе django models.Model оказалось максимально интересным как для разработки — это просто, так и для быстродействия — это быстро.
  • DRF-сериализаторы, похоже, переоценены. Но они хороши для быстрой разработки проекта.
  • Использование PYDANTIC в тестовом DJANGO-DRF проекте не показало сильных плюсов по скорости работы, и, могу предположить, что, из-за инородности идеологии, это может сильно усложнить разработку DJANGO-проекта.

В завершение добавлю, что любая технология, какая бы она ни была интересная, имеет еще контекст применения. И как бы не оргазмировал на камеру очередной фанат статической типизации, стоит сначала проверить применимость новинки и получаемые бенефиты непосредственно для вашего проекта, прежде чем биться со сложностями внедрения инородной технологии.

А теперь я предлагаю читателям в комментариях поразмышлять, зачем действительно может быть нужен PYDANTIC в DJANGO?

P.S. Еще один смешной момент той же презентации, когда один слушатель спросил у докладчика, почему тот, вместо PYDANTIC, не возьмет django-ninja?
Согласен, что нет разницы, какую малоприменимую технологию использовать. django-ninja построена в стилистике FASTAPI и тоже не умеет работать с DJANGO моделями напрямую, что, собственно, честно указано на сайте:

Models Django to Schemas django-ninja.
This is just a proposal, and it is not present in library code, but eventually this can be a part of Django Ninja.

P.P.S. Большой дисклеймер о том, что все персонажи из статьи являются вымышленными, и любое совпадение с реально живущими или жившими людьми не случайно.

P.P.P.S. Огромное спасибо моему терпеливому коллеге, Павлу П., который является первым тестером всех моих сумасшедших идей, и, в том числе, этого проекта.


ссылка на оригинал статьи https://habr.com/ru/articles/568556/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *