Хабрчане, привет! Это Леша Жиряков из МТС Диджитал. Недавно я писал про FastAPI vs Litestar и Polars vs Pandas, а сегодня разберем два популярных инструмента — Msgspec и DataClasses. Оба помогают структурировать данные, добавить энтерпрайзности в проект, но подходы у них разные. Какой из них быстрее и удобнее, где их лучше применять? Давайте разбираться.
DataClasses: классика жанра
![Источник Источник](https://habrastorage.org/getpro/habr/upload_files/287/06f/d23/28706fd2356bf3844ebdd5c6d2f9eae6.jpg)
DataClasses — встроенный инструмент Python, представленный в стандартной библиотеке начиная с версии 3.7. Он создан для упрощения и автоматизации работы с классами, которые в основном используются для хранения и обмена данными. А еще улучшает типизацию в более-менее серьезных проектах.
По своей сути это декоратор, который автоматически генерирует методы init, repr, eq и другие для вашего класса. Это избавляет разработчика от написания шаблонного кода и экономит гору времени.
Где применяется
Структурирование данных: DataClasses идеально подходит для создания DTO (Data Transfer Objects) или моделей данных.
Читаемость кода: упрощает понимание структуры данных за счет минималистичного синтаксиса.
Интеграция с другими инструментами: отлично работает с ORM, API и прочими библиотеками.
А теперь — пример использования DataClasses. Допустим, нам нужно создать DTO для пользователя:
from dataclasses import dataclass
@dataclass
class User:
first_name: str
last_name: str
email: str
age: int
Попробуем создать объект:
>>> User("Alyosha", "Popovich", "aleshap@something.com", "16")
User(first_name='Alyosha', last_name='Popovich', email='aleshap@something.com', age='16')
Получилось, но возраст должен быть int, а не str. И ошибки валидации мы не увидели. Исправим это при помощи метода __post_init__.
from dataclasses import dataclass
@dataclass
class User:
first_name: str
last_name: str
email: str
age: int
def __post_init__(self):
if not isinstance(self.age, int):
raise Exception("Age must be an integer")
Попробуем еще раз:
>>> User("Alyosha", "Popovich", "aleshap@something.com", "16")
Traceback (most recent call last):
File "<python-input-13>", line 1, in <module>
User("Alyosha", "Popovich", "aleshap@something.com", "16")
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<string>", line 7, in __init__
File "<python-input-11>", line 10, in __post_init__
raise Exception("Age must be an integer")
Exception: Age must be an integer
Ура! Теперь у нас работает валидация. А еще мы можем «заморозить» наш класс при помощи frozen=True:
from dataclasses import dataclass
@dataclass(frozen=True)
class User:
first_name: str
last_name: str
email: str
age: int
def __post_init__(self):
if not isinstance(self.age, int):
raise Exception("Age must be an integer")
Тогда нельзя будет изменить атрибуты экземпляра класса после его инициализации
>>> user_1 = User("Alyosha", "Popovich", "aleshap@something.com", 16)
>>> user_1.first_name
'Alyosha'
>>> user_1.first_name = "Иван"
Traceback (most recent call last):
File "<python-input-13>", line 1, in <module>
user_1.first_name = "Иван"
^^^^^^^^^^^^^^^^^
File "<string>", line 19, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'first_name'
Производительность
Скорость сериализации и десериализации. DataClasses не предназначен для высокоскоростной сериализации. Он использует стандартные механизмы Python для преобразования объектов в словари или JSON. Это удобно, но не быстро.
Сериализация: преобразование объектов dataclasses в JSON или словарь выполняется встроенными методами Python, например dataclasses.asdict(). Но метод рекурсивно копирует данные — а это может привести к увеличенному потреблению памяти и снижению производительности на больших структурах.
Десериализация: обратный процесс (из JSON или словаря в объект) тоже требует дополнительных шагов: создание экземпляра класса и заполнение его полей.
Использование памяти. DataClasses создает объекты, которые хранят данные в виде атрибутов. Это удобно, но не всегда эффективно с точки зрения памяти.
Каждый объект DataClasses содержит метаданные (например, имена полей). Это увеличивает объем используемой памяти.
При сериализации в JSON или словарь создаются промежуточные структуры данных, также занимающие память.
Нагрузка на CPU. DataClasses полагается на стандартные механизмы Python, они не всегда оптимизированы для производительности.
Сериализация и десериализация требуют множества операций, таких как рекурсивный обход объектов. Это увеличивает нагрузку на CPU.
Использование встроенных функций Python (например, json.dumps() или DataClasseses.asdict()) добавляет накладные расходы.
DataClasses не оптимизирован для высокой производительности при сериализациии и десериализации. Он создан для удобства, а не скорости.
Msgspec: скорость и минимализм
![Источник Источник](https://habrastorage.org/getpro/habr/upload_files/f1c/6db/d31/f1c6dbd3161d6b5213ce778f35f9be49.jpg)
Msgspec создал разработчик Джим Крист-Хариф (Jim Crist-Harif). Это библиотека для сериализации и десериализации данных с акцентом на производительность. Поддерживает форматы JSON, MessagePack и другие. Msgspec позволяет создавать типизированные структуры данных, похожие на DataClasses, но с фокусом на скорости.
Где применяется
Высоконагруженные системы: Msgspec идеально подходит для микросервисов, где важна скорость обработки данных.
Сериализация больших объемов данных: если вам нужно быстро упаковать или распаковать данные, Msgspec справится лучше многих аналогов.
Минималистичные проекты: Msgspec легковесен и не требует сложных зависимостей.
Повторим пример выше, только теперь для Msgspec:
import msgspec
class User(msgspec.Struct):
first_name: str
last_name: str
email: str
age: int
Инициализируем объект и указываем вместо целого числа для параметра age — строку.
>>> User("Alyosha", "Popovich", "aleshap@something.com", "16")
User(first_name='Alyosha', last_name='Popovich', email='aleshap@something.com', age='16')
Объект создается и ошибки не возникает.
Но у Msgspec есть одна особенность — типы данных проверяются в момент декодирования. Например, вот что получим, если выполним декодирование:
>>> msgspec.json.decode(b'{"first_name": "Alyosha", "last_name": "Popovich", "email": "aleshap@something.com", "age": "16"}', type=User)
Traceback (most recent call last):
File "<python-input-8>", line 1, in <module>
msgspec.json.decode(b'{"first_name": "Alyosha", "last_name": "Popovich", "email": "aleshap@something.com", "age": "16"}', type=User)
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
msgspec.ValidationError: Expected `int`, got `str` - at `$.age`
У нас ошибка валидации на месте age.
Как можно заметить, при самом процессе инициализации никакой валидации нет. Тут тоже можно воспользоваться методом __post_init__.
import msgspec
class User(msgspec.Struct):
first_name: str
last_name: str
email: str
age: int
def __post_init__(self):
if not isinstance(self.age, int):
raise Exception("Age must be an integer")
При создании объекта с некорректным типом данных у поля age получаем ошибку:
>>> User("Alyosha", "Popovich", "aleshap@something.com", "16")
Traceback (most recent call last):
File "<python-input-3>", line 1, in <module>
User("Alyosha", "Popovich", "aleshap@something.com", "16")
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<python-input-2>", line 9, in __post_init__
raise Exception("Age must be an integer")
Exception: Age must be an integer.
Скорость сериализации и десериализации. Msgspec разработан с акцентом на производительность. Он использует оптимизированные алгоритмы для сериализации и десериализации данных.
Сериализация: Msgspec напрямую преобразует объекты в бинарные форматы (например, MessagePack) или JSON, минимизируя использование памяти и CPU.
Десериализация: процесс обратного преобразования оптимизирован, так как Msgspec применяет низкоуровневые операции для обработки данных.
Использование памяти. Msgspec оптимизирован для работы с памятью. Он использует компактные структуры данных и минимизирует накладные расходы.
Объекты Msgspec хранят данные в более компактном формате, а это снижает использование памяти.
При сериализации Msgspec избегает создания промежуточных структур, что экономит память.
Нагрузка на CPU. Msgspec использует низкоуровневые оптимизации, чтобы снизить нагрузку на CPU:
Алгоритмы Msgspec минимизируют количество операций, необходимых для сериализации и десериализации.
Библиотека написана на C (или использует C-расширения) и работает быстро.
Msgspec гораздо быстрее DataClasses — за счет оптимизированного низкоуровневого кода и минималистичного подхода.
Что в итоге
Вот сравнительная таблица производительности между msgspec.Struct и стандартными Python dataclass. Данные взяты из официальных бенчмарков msgspec и дополнительных тестов:
Операция | msgspec.Struct | dataclass | Разница |
Импорт | 12.51 | 506.09 | ~40x |
Создание | 0.09 | 0.36 | ~4x |
Сравнение | 0.02 | 0.14 | ~7x |
Сравнение | 0.03 | 0.16 | ~5x |
Мкс — микросекунды
Результаты показывают, что msgspec.Struct превосходит стандартные dataclass по всем измеренным параметрам. Так что выбор между ними зависит от особенностей вашего проекта и личных предпочтений.
Если нужны удобство и читаемость, вы работаете с небольшими объемами данных, то DataClasses — отличный выбор. Модуль прост в использовании и интегрируется практически с любыми инструментами Python.
Если же цель — максимальная производительность и вы работаете с большими объемами данных или высоконагруженными системами, выбирайте Msgspec. Он быстрее, легковеснее и лучше подходит для задач, где каждая миллисекунда на счету.
В конечном счете оба инструмента достойны внимания, их можно использовать вместе, если того требует проект. Главное — понимать, какой модуль лучше подходит для конкретной задачи.
Что еще почитать: