Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 7 из 20 Средний 15 мин 100 XP

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 и при его загрузке:

marshal перевозит кодовый объект
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 — тоже:

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 перевёз кодовый объект туда и обратно. Это и есть его ниша: внутренний транспорт для того, что обычным форматам не по силам.

marshaljsonpickle
НазначениеВнутренние нужды CPythonОбмен данными, вебОбъекты Python в файл
Кодовые объектыДа, главный случайНетНет
Привязка к версииСтрогаяНетСлабая
Читается другими языкамиНетДаНет
Доверенные данныеТолько своиДаТолько свои

Строка про версию — ключевое отличие: формат marshal меняется вместе с байткодом. Байты, собранные на 3.12, другой интерпретатор не прочтёт — по той же причине, по которой pyc не живёт между версиями: один формат, одна версия. У json привязки нет вообще, у pickle она мягче, но тоже есть.

тело настоящего pyc из консоли
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
Командная строка недоступна в песочнице: команда выполняется на своей машине. Тело настоящего pyc читается тем же marshal.loads от 16-го байта, что и в блоках выше; метка версии в имени файла (cpython-312) зависит от вашего интерпретатора.
куда интерпретатор положит pyc
python -c "from importlib.util import cache_from_source; print(cache_from_source('report.py'))"
Вывод
__pycache__/report.cpython-312.pyc
Функция cache_from_source строит путь кэша по метке версии вашего интерпретатора, поэтому вывод зависит от машины: на CPython 3.13 хвост будет cpython-313, а разделитель пути на Windows — обратный слэш. Это та же формула имени, что проверялась в пятом уроке.

Версия формата

У формата marshal есть собственный номер версии — marshal.version. Он меняется крайне редко и описывает правила упаковки, а не версию Python:

версия формата marshal
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)
Проверь себя
0 / 7

1. Что делает marshal.dumps?

2. Какой объект marshal перевозит, ради чего формат и создан?

3. Почему тело pyc хранится в marshal, а не в json?

4. Что означает строгая привязка marshal к версии Python?

5. В каком режиме открывается файл для marshal.dump?

6. marshal.loads на файле из ненадёжного источника — плохая идея. Почему?

7. Что показывает marshal.version?

Карточки терминов
Запомнено: 0 / 7
Практика

Сериализуйте словарь {'город': 'Тверь', 'год': 2024} через marshal, восстановите его и напечатайте два факта: тип упаковки и равенство восстановленного словаря исходному.

practice.py
Вопросы и ответы по уроку

Что такое 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.