pyc-файл: устройство скомпилированного файла Python
pyc — это кодовый объект, упакованный в байты: 16 байт заголовка с magic-числом, временем и размером исходника, дальше marshal-блок с байткодом. Разбираем файл до последнего байта.
Редакция Питоники
После первого запуска любого импортируемого модуля рядом с ним появляется каталог __pycache__, а в нём — файл с расширением .pyc. Текстовым редактором его не открыть: внутри не текст, а скомпилированный байткод из первого урока, упакованный в байты вместе с паспортом программы. pyc — это кэш компиляции: интерпретатор кладёт его рядом с исходником, чтобы в следующий раз не компилировать тот же код заново. В этом уроке мы создадим pyc сами и прочтём его до последнего байта — формат проще, чем кажется.
Создаём первый pyc
Для опытов возьмём py_compile — модуль стандартной библиотеки, который компилирует файл и пишет результат на диск. Подробный разбор его настроек — в третьем уроке, здесь он просто поставщик подопытного файла:
import os
import py_compile
with open('app_demo.py', 'w', encoding='utf-8') as f:
f.write('RATIO = 2\n\n\ndef scaled(n):\n return n * RATIO\n')
py_compile.compile('app_demo.py')
print('исходник на месте:', os.path.exists('app_demo.py'))
print('каталог pyc появился:', os.path.isdir('__pycache__'))
import glob
for p in glob.glob('__pycache__/*.pyc'):
os.remove(p)
if os.path.isdir('__pycache__'):
os.rmdir('__pycache__')
os.remove('app_demo.py')
исходник на месте: True каталог pyc появился: True
Компиляция положила результат в каталог __pycache__ рядом с исходником. Имя файла внутри повторяет имя модуля и добавляет метку версии интерпретатора — у CPython 3.12 она выглядит как cpython-312, поэтому мы не печатаем имя целиком: на вашей машине хвост будет другим. Структура при этом одна и та же на всех версиях начиная с 3.7: 16 байт заголовка и marshal-блок.
Заголовок: первые 16 байт
Заголовок pyc — четыре числа по четыре байта каждое, записанные в младшем порядке (little-endian). Первое — magic-число версии, а три остальных — паспорт исходника:
| Смещение | Размер | Поле | Содержимое |
|---|---|---|---|
| 0–3 | 4 байта | Magic | Число версии Python, записавшей файл |
| 4–7 | 4 байта | Флаги | 0 для обычных pyc, отдельные биты для hash-режима |
| 8–11 | 4 байта | Время | mtime исходника на момент компиляции |
| 12–15 | 4 байта | Размер | Размер исходника в байтах |
Читается заголовок обычной работой с байтами: читаем файл в режиме rb, режем срезами по четыре байта и собираем числа через int.from_bytes(..., 'little'). Первый опыт — сверить magic-число файла с тем, что выдает наш собственный интерпретатор через importlib.util.MAGIC_NUMBER:
import importlib.util
import os
import py_compile
with open('header_demo.py', 'w', encoding='utf-8') as f:
f.write('N = 1\n')
pyc = py_compile.compile('header_demo.py')
with open(pyc, 'rb') as f:
data = f.read()
print('файл прочитан, данных больше нуля:', len(data) > 0)
print('первые 4 байта совпали с MAGIC_NUMBER:', data[:4] == importlib.util.MAGIC_NUMBER)
print('магическое число занимает 4 байта:', len(importlib.util.MAGIC_NUMBER) == 4)
print('флаги равны нулю:', int.from_bytes(data[4:8], 'little') == 0)
print('время исходника записано:', int.from_bytes(data[8:12], 'little') > 0)
import glob
for p in glob.glob('__pycache__/*.pyc'):
os.remove(p)
if os.path.isdir('__pycache__'):
os.rmdir('__pycache__')
os.remove('header_demo.py')
файл прочитан, данных больше нуля: True первые 4 байта совпали с MAGIC_NUMBER: True магическое число занимает 4 байта: True флаги равны нулю: True время исходника записано: True
Пять проверок, пять True. Magic-число самого файла совпало с magic-числом работающего интерпретатора — это и есть та самая проверка версии, из-за которой pyc с одной версии не запускается на другой (пятый урок). Флаги равны нулю — файл собран в обычном временном режиме, а не в hash-режиме для деплоя. Поле времени — не ноль: интерпретатор записал mtime исходника на момент компиляции.
import glob
import os
import py_compile
with open('size_demo.py', 'w', encoding='utf-8') as f:
f.write('N = 1\nN += 2\n')
pyc = py_compile.compile('size_demo.py')
with open(pyc, 'rb') as f:
data = f.read()
stored = int.from_bytes(data[12:16], 'little')
print('размер из заголовка совпал с исходником:', stored == os.path.getsize('size_demo.py'))
for p in glob.glob('__pycache__/*.pyc'):
os.remove(p)
if os.path.isdir('__pycache__'):
os.rmdir('__pycache__')
os.remove('size_demo.py')
размер из заголовка совпал с исходником: True
В заголовке лежит точный размер исходника в байтах: N = 1 и N += 2 с переводами строк — тринадцать байт, и os.path.getsize дал то же число. Пара «время + размер» — это те самые два поля, по которым интерпретатор решает, устарел ли кэш: тема шестого урока про __pycache__.
Как выглядит заголовок в байтах
Вот настоящий дамп первых 16 байт pyc для исходника N = 1 (7 байт текста), разбитый на поля:
f3 0d 0d 0a magic
00 00 00 00 флаги = 0
d5 65 f0 6a время изменения исходника
07 00 00 00 размер исходника = 7 байт
Первые два байта magic меняются от версии к версии, последние два всегда 0d 0a — перевод строки в стиле Windows: это история формата, оставшаяся с ранних версий. Время d5 65 f0 6a — просто unix-timestamp компиляции. Проверить этот дамп на своей машине можно тем же кодом из предыдущего блока — только байты будут свои.
Тело файла: marshal-блок
С 16-го байта начинается тело — marshal-сериализация кодового объекта из первого урока. Marshal — встроенный формат CPython для своих объектов; его тонкости — седьмой урок. Здесь достаточно того, что marshal.loads превращает тело обратно в кодовый объект, который можно исполнить:
import glob
import marshal
import os
import py_compile
import types
with open('body_demo.py', 'w', encoding='utf-8') as f:
f.write('def area(n):\n return n * n\n')
pyc = py_compile.compile('body_demo.py')
with open(pyc, 'rb') as f:
data = f.read()
code = marshal.loads(data[16:])
print('тело pyc — кодовый объект:', isinstance(code, types.CodeType))
print('имя кодового объекта модуля:', code.co_name)
ns = {}
exec(code, ns)
print('функция из pyc работает:', ns['area'](5))
for p in glob.glob('__pycache__/*.pyc'):
os.remove(p)
if os.path.isdir('__pycache__'):
os.rmdir('__pycache__')
os.remove('body_demo.py')
тело pyc — кодовый объект: True имя кодового объекта модуля: <module> функция из pyc работает: 25
Три шага — и pyc полностью расшифрован: срез [16:] отделил заголовок, marshal.loads собрал кодовый объект, exec исполнил его как обычный модуль. Функция area из файла, который мы никогда не импортировали, честно вернула 25. Именно эти три шага выполняет интерпретатор, когда грузит модуль из кэша, — только заголовок он ещё и проверяет.
Чего внутри pyc нет
Внутри только байткод и паспорт исходника. Нет исходного текста — pyc не обфускация и не архив: вернуть исходник из байткода можно лишь декомпилятором конкретной версии, с потерями. Нет и ускорения исполнения: байткод тот же, что компилировался бы на лету; pyc экономит только время компиляции при загрузке модуля.
import os
import py_compile
with open('rebuild_demo.py', 'w', encoding='utf-8') as f:
f.write('K = 3\n')
pyc = py_compile.compile('rebuild_demo.py')
os.remove(pyc)
print('pyc после удаления существует:', os.path.exists(pyc))
py_compile.compile('rebuild_demo.py')
print('pyc после повторной компиляции:', os.path.exists(pyc))
os.remove(pyc)
os.rmdir('__pycache__')
os.remove('rebuild_demo.py')
pyc после удаления существует: False pyc после повторной компиляции: True
Удалённый pyc — не потеря: исходник на месте, компилятор соберёт файл заново при первом же обращении. Поэтому каталог __pycache__ смело исключают из git и чистят при сборке дистрибутива — в шестом уроке это разобрано вместе с правилами инвалидации.
python -m dis __pycache__/app_demo.cpython-312.pyc
python -c "data = open('__pycache__/app_demo.cpython-312.pyc', 'rb').read(16); print(data[4:8] == b'\x00\x00\x00\x00')"
True
- pyc — кэш компиляции: кодовый объект, записанный на диск рядом с исходником;
- заголовок — 16 байт: magic версии, флаги, mtime и размер исходника, все числа little-endian;
- тело — marshal-блок: marshal.loads(data[16:]) возвращает кодовый объект, exec исполняет его;
- исходного текста внутри нет, ускоряется только загрузка модуля, а не его исполнение;
- pyc привязан к версии: magic-число и формат байткода меняются в каждой минорной версии.
Компилировать файлы руками не обязательно — интерпретатор делает это сам при импорте. Но для сборки дистрибутива, проверки синтаксиса без запуска и подготовки кэша у стандартной библиотеки есть два инструмента: py_compile и compileall в третьем уроке.
Сначала предскажи ответ в голове — это главный навык программиста.
import importlib.util
print(len(importlib.util.MAGIC_NUMBER))
data = bytes([0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 7, 0, 0, 0])
print(int.from_bytes(data[8:12], 'little') > 0)
print(int.from_bytes(data[12:16], 'little'))
import marshal, types
data = b'\x00' * 16
print(isinstance(marshal.loads(data), types.CodeType))
1. Из каких частей состоит pyc-файл?
2. Что хранится в первых 4 байтах pyc?
3. Как из pyc получить кодовый объект?
4. Что экономит pyc при импорте модуля?
5. Зачем в заголовке pyc время и размер исходника?
6. Исходный текст программы внутри pyc?
7. Что произойдёт после удаления pyc при работающем исходнике?
Скомпилируйте файл demo.py со строкой N = 5, прочитайте заголовок pyc и проверьте, что размер исходника из заголовка совпал с реальным размером файла. Напечатайте результат проверки.
Что такое pyc файл простыми словами?
Это кэш компиляции: Python переводит исходник в байткод и сохраняет результат рядом с модулем, чтобы при следующем импорте пропустить компиляцию. Внутри 16 байт заголовка (версия, флаги, время, размер исходника) и marshal-блок с кодовым объектом.
Урок 3: py_compile и compileallКак открыть или прочитать pyc файл?
Текстовым редактором — никак: это бинарный файл. Содержимое смотрят дизассемблером (python -m dis файл.pyc) или читают программно: marshal.loads(data[16:]) вернёт кодовый объект, у которого доступны co_name, co_consts и байткод co_code.
Урок 4: дизассемблер disПочему pyc не запускается на другой версии Python?
В заголовке записано magic-число версии, и оно меняется с каждым минорным релизом. Интерпретатор сверяет его со своим и отказывается грузить файл с чужим числом. Внутри, к тому же, байткод другой версии — простая правка заголовка не поможет.
Урок 5: версии Python и magic-числоМожно ли запускать программу только из pyc без исходника?
Да, если pyc собран ровно той версией интерпретатора: загрузчик SourcelessFileLoader грузит модули без исходника. Но это не защита кода и не ускорение: байткод декомпилируется инструментами той же версии, а исполняется не быстрее исходника.
Чем pyc отличается от exe?
pyc — кэш байткода для того же интерпретатора Python, exe — самодостаточная программа, в которую PyInstaller или Nuitka упаковывают интерпретатор или компилируют код в машинные команды. pyc без Python не запускается.
Урок 10: сборка exe через PyInstallerПонравился урок? Сошлитесь на него
«pyc — это кэш компиляции: интерпретатор кладёт его рядом с исходником, чтобы в следующий раз не компилировать тот же код заново.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Компиляция · Урок 1
Как Python исполняет код: байткод и виртуальная машина
Питон исполняет программу в два этапа: компилятор переводит исходник в байткод, виртуальная машина выполняет его по инструкциям. Достаём оба этапа на поверхность и вызываем компилятор сами.
Компиляция · Урок 3
py_compile и compileall: компилируем Python сами
Два инструмента стандартной библиотеки компилируют код без запуска: py_compile — один файл, compileall — целый каталог. Собираем pyc вручную и ловим ошибки компиляции.
Компиляция · Урок 5
Версии Python и magic-число: почему pyc не совместим
Каждая минорная версия CPython получает новое magic-число, и pyc одной версии не грузится другой. Находим число в importlib, сверяем с заголовком pyc и проверяем версию кодом.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
pytest · Урок 11
Встроенные фикстуры: capsys и tmp_path
Половину бытовых задач тестирования закрывают две фикстуры из коробки: capsys читает то, что функция напечатала, tmp_path выдаёт чистую временную папку — без декораторов и настроек.
pytest временный файл тестpytest тест файла
json · Урок 17
Сохраняем результаты: json.dump итоговых данных
Отфильтровали, отсортировали, посчитали — теперь результат должен пережить скрипт: json.dump пишет итог в файл с кириллицей и лесенкой.
python сохранить результат в json файлpython записать список словарей в файл
Компиляция · Урок 15
Декомпиляция pyc в Python: что видно из байткода
pyc не содержит исходного текста, но хранит имена, константы и структуру. Разбираем, что восстановит декомпилятор, что потеряно навсегда и где проходит этическая граница.
python байткод декомпиляцияpyc файл декомпиляция