Несколько дней назад я решил разобраться в устройстве
msgspec. Получилось како бычно: я напал на него со своими PRами, мне через день выдали права на merge и release. Но самое главное: теперь я могу рассказать вам про внутреннее устройство самого быстрого сериализатора для json в питоне.Как быстро распарсить json?
Традиционные парсеры json делают так:
- Парсим весь json документ
- Используем промежуточный слой для хранения json как примитивных Python объектов: dict, list, int, str, None, тд
- Превращаем Python объекты в финальный вариант: датаклассы, модели, более сложные типы, тд
msgspec использует несколько важных хитростей, чтобы парсить json наиболее быстрым способом.Пример:
>>> import msgspec
>>> class User(msgspec.Struct):
... username: str
... email: str
...
>>> decoder = msgspec.json.Decoder(User)
>>> decoder.decode(b'{"username": "example", "email": "email@example.com"}')
User(username='example', email='email@example.com')
Все самое интересное происходит в JSONDecoder_decode и в json_decode:
1. Мы используем
TypeNode *type для мета информации о том, что мы будем парсить. В нашем случае там будет struct User с двумя str полями2. Далее мы проваливаемся в функцию json_decode_nocustom, она очень красивая:
static MS_INLINE PyObject *
json_decode_nocustom(
JSONDecoderState *self, TypeNode *type, PathNode *path
) {
// ...
switch (c) {
case 'n': return json_decode_none(self, type, path);
case 't': return json_decode_true(self, type, path);
case 'f': return json_decode_false(self, type, path);
case '[': return json_decode_array(self, type, path);
case '{': return json_decode_object(self, type, path);
case '"': return json_decode_string(self, type, path);
default: return json_maybe_decode_number(self, type, path);
}
}
Буквально по первому символу, мы можем парсить нужные части. Хитрый json_decode_object посмотрит, что
type у нас MS_TYPE_STRUCT и будет парсить сразу msgspec.Struct. Что еще более хитро, то парситься будут только те ключи, которые явно указаны в User, остальные будут просто пропускаться через вызов json_skip.То есть: ключ в C мы конечно обязаны прочитать в виде
char *, чтобы сравнить его с существующими ключами User. Но вот создавать дорогие промежуточные Python объекты мы не будем. Если ключ нам не нужен, то и значение его мы парсить не будем. На выходе получим сразу объект User без промежуточных слоев и их аллокаций. Быстро? Быстро.Минусы
На данный момент у
msgspec есть главный минус: плохая поддержка Union типов. То есть: некоторые комбинации данных вообще не получится распарсить. Например: str | bytes. Или два датакласса. Или два тайпдикта. Почему? Потому что оптимизации пока мешают работе 🌚Но, вопрос решаем. Сделаем.
Второй минус: мало всего можно выразить.
pydantic умеет куда больше. Потому я в django-modern-rest и сделал выбор сериализатора для каждого отдельного контроллера. Чтобы точечно выбирать скорость vs функциональность.Что будет с msgspec дальше?
Новые релизы добавят кучу новых фичей. Поддержку
pyrefly, heap types, поддержку subinterpreters, FT, более гибкие правила проверок значений и тд.А еще я параллельно добавил поддержку
frozendict для Python 3.15+ и предложил сделать новое АПИ для него: PyFrozenDict_FromDictSteal, потому что текущее АПИ работает за O(n * 2), когда можно за O(n).Обсуждение: а вы пробовали msgspec? Какие впечатления?
| Поддержать | YouTube | GitHub | Чат |