marshal: сериализация кодовых объектов Python
marshal — внутренний формат CPython, которым байткод упаковывается в pyc. Сериализуем кодовый объект, прочитаем его обратно и сравним marshal с json и pickle.
Редакция Питоники
В устройстве pyc после 16-байтного заголовка начинался marshal-блок: байты, из которых marshal.loads собирал кодовый объект. Осталось объяснить сам формат. Marshal — это сериализация: перевод живых объектов Python в последовательность байтов и обратно, как у json. Разница в позиции: json — внешний формат для обмена данными с любыми программами, marshal — внутренний язык CPython для своих объектов. Его главный пассажир — кодовый объект с байткодом из первого урока. Marshal — единственный сериализатор стандартной библиотеки, который перевозит кодовые объекты.
dumps и loads: байты туда, объект обратно
Пара marshal.dumps и marshal.loads работает как у json: упаковывает значение в байты и восстанавливает его. Проверим на списке:
import marshal
data = marshal.dumps([1, 2, 3])
print('тип результата dumps:', type(data).__name__)
print('байт больше нуля:', len(data) > 0)
restored = marshal.loads(data)
print('восстановлено:', restored)
тип результата dumps: bytes байт больше нуля: True восстановлено: [1, 2, 3]
dumps вернул объект bytes — мы проверяем только тип и наличие данных, не печатая сами байты: их содержимое зависит от версии Python и ничего осмысленного человеку не говорит. loads по этим байтам вернул список, равный исходному. Обычные данные marshal тоже умеет — числа, строки, словари, None:
import marshal
for value in (42, 'привет', [1, 2], {'ключ': 7}, None, True):
back = marshal.loads(marshal.dumps(value))
print(back == value, repr(back))
True 42
True 'привет'
True [1, 2]
True {'ключ': 7}
True None
True TrueШесть типов прошли туда и обратно без потерь: восстановленное значение равно исходному, а repr печатает его в привычном виде. Но это разминка: настоящее ремесло marshal — кодовые объекты, для которых он и создан.
Кодовый объект: главный пассажир
Скомпилируем функцию, упакуем её кодовый объект и прочитаем обратно. Это ровно то, что происходит при записи pyc и при его загрузке:
import marshal
import types
code = compile('def triple(n):\n return n * 3\n', '<модуль>', 'exec')
data = marshal.dumps(code)
loaded = marshal.loads(data)
print('это кодовый объект:', isinstance(loaded, types.CodeType))
print('имя кодового объекта:', loaded.co_name)
ns = {}
exec(loaded, ns)
print('функция из marshal:', ns['triple'](14))
это кодовый объект: True имя кодового объекта: <module> функция из marshal: 42
Цепочка замкнулась: compile дал кодовый объект, marshal свернул его в байты, loads собрал обратно — и exec исполнил восстановленную функцию, вернувшую 42. Между dumps и loads объект побывал в виде байтов, которые можно записать в файл, передать по сети, положить в pyc. Для работы с файлами у marshal есть короткие формы dump и load:
import marshal
import os
code = compile('RATE = 7\n\ndef calc(n):\n return n * RATE\n', '<программа>', 'exec')
with open('program.marsh', 'wb') as f:
marshal.dump(code, f)
print('файл marshal создан:', os.path.exists('program.marsh'))
with open('program.marsh', 'rb') as f:
loaded = marshal.load(f)
ns = {}
exec(loaded, ns)
print('функция из файла:', ns['calc'](6))
os.remove('program.marsh')
файл marshal создан: True функция из файла: 42
Файл открылся в бинарных режимах — wb на запись, rb на чтение: marshal пишет байты, а не текст. Внутри program.marsh — тот же формат, что в теле pyc-файла, только без 16-байтного заголовка. Меняя имя на module.cpython-312.pyc и дописывая заголовок, мы получили бы настоящий кэш компиляции.
marshal против json и pickle
Почему для pyc не взяли json или pickle — популярные форматы стандартной библиотеки? Ответ виден на кодовом объекте: json не умеет его в принципе, pickle — тоже:
import json
import marshal
import pickle
code = compile('x = 1\n', '<модуль>', 'exec')
try:
json.dumps(code)
except TypeError:
print('json отказался от кодового объекта: TypeError')
try:
pickle.dumps(code)
except TypeError:
print('pickle тоже отказался: TypeError')
data = marshal.dumps(code)
print('marshal справился, байт больше нуля:', len(data) > 0)
print('вернулся кодовый объект:', isinstance(marshal.loads(data), type(code)))
json отказался от кодового объекта: TypeError pickle тоже отказался: TypeError marshal справился, байт больше нуля: True вернулся кодовый объект: True
Два сериализатора бросили TypeError, и только marshal перевёз кодовый объект туда и обратно. Это и есть его ниша: внутренний транспорт для того, что обычным форматам не по силам.
| marshal | json | pickle | |
|---|---|---|---|
| Назначение | Внутренние нужды CPython | Обмен данными, веб | Объекты Python в файл |
| Кодовые объекты | Да, главный случай | Нет | Нет |
| Привязка к версии | Строгая | Нет | Слабая |
| Читается другими языками | Нет | Да | Нет |
| Доверенные данные | Только свои | Да | Только свои |
Строка про версию — ключевое отличие: формат marshal меняется вместе с байткодом. Байты, собранные на 3.12, другой интерпретатор не прочтёт — по той же причине, по которой pyc не живёт между версиями: один формат, одна версия. У json привязки нет вообще, у pickle она мягче, но тоже есть.
python -c "import marshal, sys, types; data = open(sys.argv[1], 'rb').read()[16:]; code = marshal.loads(data); print(isinstance(code, types.CodeType))" __pycache__/report.cpython-312.pyc
True
python -c "from importlib.util import cache_from_source; print(cache_from_source('report.py'))"
__pycache__/report.cpython-312.pyc
Версия формата
У формата marshal есть собственный номер версии — marshal.version. Он меняется крайне редко и описывает правила упаковки, а не версию Python:
import marshal
print('версия формата marshal:', marshal.version)
версия формата marshal: 4
Версия 4 держится с Python 3.4 и менять её не спешат: формат привязан к байткоду, а тот меняется сам. Практический вывод для данных прост: marshal не для долгого хранения. Кодовые объекты живут в pyc ровно до следующей версии интерпретатора, обычные данные — лучше в json.
- marshal — сериализация CPython: dumps упаковывает объект в байты, loads собирает обратно;
- единственный формат стандартной библиотеки, который перевозит кодовые объекты;
- marshal.dump и marshal.load работают с файлами в бинарных режимах wb и rb;
- формат строго привязан к версии Python: байты 3.12 другой интерпретатор не прочтёт;
- для обмена данными marshal не предназначен: там json, для произвольных объектов — pickle.
На этом слой компиляции полностью открыт: байткод из первого урока, заголовок pyc из второго, инструменты сборки из третьего и формат тела — marshal — из этого. Дальше раздел переходит к упаковке программ: восьмой урок про zipapp собирает скрипт в запускаемый архив .pyz.
Сначала предскажи ответ в голове — это главный навык программиста.
import marshal
data = marshal.dumps({'a': 1})
print(type(data).__name__)
print(marshal.loads(data) == {'a': 1})
import json
def make():
return 5
try:
json.dumps(make.__code__)
print('получилось')
except TypeError:
print('отказ')
import marshal
import types
code = compile('V = 4\n', '<x>', 'exec')
back = marshal.loads(marshal.dumps(code))
print(isinstance(back, types.CodeType))
print(back.co_name)
1. Что делает marshal.dumps?
2. Какой объект marshal перевозит, ради чего формат и создан?
3. Почему тело pyc хранится в marshal, а не в json?
4. Что означает строгая привязка marshal к версии Python?
5. В каком режиме открывается файл для marshal.dump?
6. marshal.loads на файле из ненадёжного источника — плохая идея. Почему?
7. Что показывает marshal.version?
Сериализуйте словарь {'город': 'Тверь', 'год': 2024} через marshal, восстановите его и напечатайте два факта: тип упаковки и равенство восстановленного словаря исходному.
Что такое marshal в Python простыми словами?
Это встроенный упаковщик CPython: marshal.dumps переводит объект в байты, marshal.loads собирает обратно. Формат существует для внутренних нужд интерпретатора — прежде всего для хранения байткода в pyc-файлах.
Урок 2: устройство pyc-файлаЧем marshal отличается от json и когда что использовать?
json — текстовый формат для обмена данными с любыми программами: читается везде, но умеет только данные. Marshal — бинарный внутренний формат: умеет кодовые объекты, но привязан к версии Python и не предназначен для обмена. Для данных — json, для байткода — marshal.
Безопасно ли читать marshal-данные из чужого файла?
Нет: как и pickle, marshal при восстановлении объектов может исполнить нежелательный код. Читайте только данные, созданные вашим интерпретатором. Плюс несовместимость версий: чужой файл, скорее всего, не прочитается вовсе.
Почему marshal-блок pyc не читается на другой версии Python?
Формат marshal следует за байткодом, а байткод меняется в каждой минорной версии. Байты, собранные на 3.12, интерпретатор 3.13 соберёт в бессмысленный объект или откажется — та же причина, по которой pyc не переносится между версиями.
Урок 5: версии Python и magic-числоГде marshal используется на практике?
Внутри каждого pyc — тело файла это marshal-данные. В frozen-модулях CPython — часть стандартной библиотеки зашита в бинарник marshal-блоками. В сборщиках вроде PyInstaller — упаковка байткода модулей в итоговый файл.
Понравился урок? Сошлитесь на него
«Marshal — единственный сериализатор стандартной библиотеки, который перевозит кодовые объекты.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Компиляция · Урок 2
pyc-файл: устройство скомпилированного файла Python
pyc — это кодовый объект, упакованный в байты: 16 байт заголовка с magic-числом, временем и размером исходника, дальше marshal-блок с байткодом. Разбираем файл до последнего байта.
Компиляция · Урок 5
Версии Python и magic-число: почему pyc не совместим
Каждая минорная версия CPython получает новое magic-число, и pyc одной версии не грузится другой. Находим число в importlib, сверяем с заголовком pyc и проверяем версию кодом.
Компиляция · Урок 8
zipapp: скрипт-архив .pyz — программа в одном файле
Один файл вместо папки: собираем запускаемый zip-архив с точкой входа __main__.py, дописываем shebang и разбираем, когда .pyz достаточно без exe.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
json · Урок 1
Что такое JSON и зачем он Python: первый json.dumps
Первое превращение словаря в json-строку одной командой: import json, json.dumps и честный взгляд на кракозябры в выводе — всё исполняется прямо на странице.
json python что эточто такое json
Словари · Урок 16
Словарь и JSON: json.loads и json.dumps
Словарь и JSON — два облика одних данных: json.loads читает строку в dict, json.dumps собирает строку из словаря, json.dump пишет в файл, а ensure_ascii=False бережёт кириллицу.
python json словарьjson.loads python
Компиляция · Урок 12
Nuitka: компиляция Python-кода в C
От упаковки к компиляции: как Nuitka переводит программу в C, что становится машинным кодом, а что остаётся PyObject — и где ждать ускорения.
nuitka против pyinstaller