
Было замечательное теплое австрийское утро, и ничего не предвещало … ничего, пока мой коллега не порекомендовал мне посмотреть запись недавно прошедшей 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 если была ошибка
Если добравшийся до этого момента читатель спросит: это все? Я отвечу, что есть еще варианты сериализаторов. Каждый из вас может добавить свой вариант в код репозитория.
Следующие шаги:
- Создаем несколько объектов Django-модели Organisation
- Получаем сериализованный лист объектов из базы по GET-запросу
- Отправляем его обратно POST-запросом
- Получаем набор сериализованных объектов
- Сравниваем время работы всех объявленых сериализаторов.
- Профит
В репозитории в папке TEST вы найдете файл, который выполняет все действия, команды запуска в readme. Можно настроить количество сериализуемых объектов, печать в консоль и добавление ошибок в объекты.
Момент истины

Создавалось 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
Безоговорочным победителем сериализации объектов из базы в строку 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/
Добавить комментарий