Любой, кто работал с большими JSON-файлами в Python, знает, как стремительно может закончиться оперативная память. А если вы пользуетесь Pydantic, который завоевал популярность благодаря удобству и простоте, то, скорее всего, уже сталкивались с неприятной ситуацией, когда даже средний по размеру файл превращается в прожорливого монстра, способного съесть всю доступную память.
Недавно Итамар Тернер-Троринг поделился в блоге Python⇒Speed полезными рекомендациями, как победить эту проблему и не исчерпать ресурсы памяти при работе с Pydantic и большими JSON-файлами. Я внимательно изучил его подходы и хочу дополнить их собственным опытом.
📛 Почему вообще возникает проблема?
Возьмём JSON-файл размером около 100 мегабайт. Казалось бы, небольшой объём данных. Однако, если вы просто прочитаете этот файл и загрузите его в модель Pydantic через стандартный метод:
directory = CustomerDirectory.model_validate_json(raw_json)
то неожиданно обнаружите, что приложение потребляет аж до 2000 MB памяти! 🤯 То есть в 20 раз больше размера исходного файла. Это происходит по двум причинам:
- 🐘 Парсинг JSON: большинство библиотек целиком загружают JSON-файл в память.
- 🧱 Создание объектов Python: стандартные классы Python не очень экономны по памяти, особенно если объектов много.
🛠️ Как бороться с прожорливостью?
Итамар предлагает три подхода, которые, применённые вместе, дают впечатляющие результаты.
🕹️ Способ 1: Стриминг JSON с библиотекой ijson
Первым шагом можно отказаться от обычного парсинга JSON, заменив его на инкрементальный парсинг с библиотекой ijson. Она читает файл небольшими порциями, не загружая сразу весь файл в память:
import ijson
with open("customers.json", "rb") as f:
data = {}
for cid, cust_dict in ijson.kvitems(f, ""):
customer = Customer.model_validate(cust_dict)
data[cid] = customer
directory = CustomerDirectory.model_validate(data)
✅ Результат: уже экономим память, снижая потребление с 2000 MB до 1200 MB. Правда, это замедляет процесс примерно в 5 раз, но иногда лучше медленнее и стабильнее.
🧩 Способ 2: dataclasses и магия slots
Второй приём — переход от классических моделей Pydantic (BaseModel) к использованию dataclasses с параметром slots=True. Слоты — это способ сообщить Python, что атрибуты объектов заранее известны и фиксированы, и дополнительные атрибуты добавлять нельзя. Благодаря этому объекты занимают гораздо меньше памяти.
Вот так выглядит улучшенная модель с dataclasses и слотами:
from pydantic import RootModel
from pydantic.dataclasses import dataclass
@dataclass(slots=True)
class Name:
first: str | None
last: str | None
@dataclass(slots=True)
class Customer:
id: str
name: Name
notes: str
CustomerDirectory = RootModel[dict[str, Customer]]
Теперь парсим файл аналогично, но уже с dataclass-объектами:
import ijson
with open("customers.json", "rb") as f:
data = {}
for cust_id, cust_dict in ijson.kvitems(f, ""):
customer = Customer(**cust_dict)
data[cust_id] = customer
directory = CustomerDirectory.model_validate(data)
✅ Результат: память падает ещё сильнее — с 1200 MB до 450 MB! Уже неплохо, правда?
🚀 Что ещё можно улучшить? Личный опыт
Хочу добавить, что помимо слотов и инкрементального парсинга есть и другие приёмы для улучшения работы с большими данными:
- 📦 Использование генераторов (generator) вместо полного списка объектов.
- ⚙️ Ленивое чтение данных (lazy-loading) только при необходимости обращения к ним.
- 🗃️ Хранение промежуточных данных в базах данных, таких как SQLite, вместо оперативной памяти.
Pydantic мог бы встроить аналогичную функциональность непосредственно внутрь своей архитектуры. Например, в перспективе авторы библиотеки могли бы реализовать встроенный инкрементальный парсинг и поддержку слотов без необходимости вручную использовать dataclasses.
🎯 Заключение и выводы
Проблема с памятью при парсинге JSON в Pydantic не очевидна, пока вы не столкнётесь с реальным большим файлом. Но как только столкнётесь, эти знания сэкономят вам не только гигабайты памяти, но и массу нервов. Сам подход к оптимизации, предложенный в статье Итамара, полезен не только для конкретного кейса JSON+Pydantic, но и в принципе при работе с любыми большими объёмами данных в Python.
Для меня главные выводы таковы:
- 💡 Всегда думайте о памяти заранее, если ожидаете большие данные.
- 🔧 Стриминг и слоты — простые инструменты, доступные каждому Python-разработчику.
- 🛡️ Если есть возможность, избегайте хранения всего объёма данных в памяти и используйте промежуточные хранилища и ленивое чтение.
Оптимизация — это искусство, которое позволяет писать по-настоящему производительный код даже в высокоуровневых языках вроде Python.
Не забывайте: «Память дешевле, чем раньше, но всё ещё не бесплатна!»
🔗 Полезные ссылки:
- Оригинальная статья Итамара Тернер-Троринга на Python⇒Speed:
Loading Pydantic models from JSON without running out of memory - Библиотека ijson для инкрементального парсинга JSON:
ijson на PyPI - Документация по dataclasses в Python:
dataclasses — Python Documentation - Поддержка слотов в dataclasses:
Dataclass Slots (PEP 681)