dis: дизассемблер байткода Python
dis печатает байткод функции инструкция за инструкцией: какая команда, над какими значениями, куда прыгает условие. Учимся читать колонки вывода и понимать стековую машину.
Редакция Питоники
В первом уроке мы узнали, что компилятор превращает исходник в байткод, а виртуальная машина исполняет его. Байткод при этом остался абстракцией: мы видели тип кодового объекта и его атрибуты, но не сами команды. Дизассемблер dis из стандартной библиотеки печатает их построчно — это рентген Python-программы: видно, во что компилятор превратил каждую строку и на чём именно виртуальная машина тратит время.
Перед разбором вывода соберём подопытную функцию и убедимся, что она работает как ожидается:
def discount(price):
if price >= 100:
return price * 0.8
return price
print(discount(200))
print(discount(50))
160.0 50
Первый дизассемблёр: колонки вывода
Функция dis.dis принимает кодовый объект — или функцию напрямую, байткод она берёт из __code__. Вот её байткод:
import dis
def discount(price):
if price >= 100:
return price * 0.8
return price
dis.dis(discount)
1 RESUME 0
2 LOAD_FAST 0 (price)
LOAD_CONST 1 (100)
COMPARE_OP 188 (bool(>=))
POP_JUMP_IF_FALSE 5 (to L1)
3 LOAD_FAST 0 (price)
LOAD_CONST 2 (0.8)
BINARY_OP 5 (*)
RETURN_VALUE
4 L1: LOAD_FAST 0 (price)
RETURN_VALUEРазбираем колонки слева направо. Первая — номер строки исходника: инструкции второй строки программы все помечены двойкой. Вторая — смещение инструкции внутри байткода и метки переходов вроде L1. Третья — имя команды, четвёртая — её аргумент; в скобках dis печатает человекочитаемое значение аргумента: не абстрактный индекс 0, а конкретное (price).
| Колонка | Смысл | Пример из вывода |
|---|---|---|
| Строка | Номер строки исходника | 2, 3, 4 |
| Смещение и метка | Позиция инструкции в байткоде | 5 (to L1) |
| Имя команды | Что делает VM | LOAD_FAST, COMPARE_OP |
| Аргумент | Данные команды | 0 (price), 1 (100) |
Стековая машина: как VM исполняет команды
Инструкции работают не с переменными напрямую, а со стеком — последним хранилищем «положил сверху, взял сверху». LOAD_FAST кладёт значение локальной переменной на стек, COMPARE_OP снимает два верхних значения и сравнивает их, POP_JUMP_IF_FALSE снимает результат и при нуле прыгает на метку. Проследим за сложением из простой функции:
import dis
def add(a, b):
return a + b
dis.dis(add)
1 RESUME 0
2 LOAD_FAST_LOAD_FAST 1 (a, b)
BINARY_OP 0 (+)
RETURN_VALUEТри команды на всю функцию: загрузить значения, выполнить сложение, вернуть результат. Сложение на стеке выглядит так: LOAD кладёт a и b, BINARY_OP снимает оба и кладёт обратно сумму, RETURN_VALUE снимает верхнее значение и отдаёт его вызывающему. Стековая модель объясняет порядок во всех инструкциях: всё, что нужно операции, сначала оказывается на стеке. Байткод — это план работы виртуальной машины: команды могут меняться от версии к версии, но стековая модель исполнения остаётся.
Что байткод говорит о коде
Чтение dis отвечает на вопросы, которые не видны в исходнике. Что компилятор свернул? Что попадает в константы? Сколько команд стоит за выражение? Всё это лежит в кодовом объекте рядом с байткодом — и читается обычными атрибутами:
def discount(price):
if price >= 100:
return price * 0.8
return price
code = discount.__code__
print('имя:', code.co_name)
print('параметры:', code.co_varnames)
print('константы:', code.co_consts)
имя: discount
параметры: ('price',)
константы: (None, 100, 0.8)Вывод совпал с дизассемблером: LOAD_CONST 1 (100) и LOAD_CONST 2 (0.8) берут значения из кортежа констант по индексам 1 и 2. None в начале — заготовка возврата по умолчанию. Атрибуты co_varnames и co_consts стабильны между версиями — в отличие от самих инструкций, поэтому инспекцию кода строят на них. Тот же приём на функции с локальной переменной:
def report(total):
bonus = 10
return total + bonus
code = report.__code__
print('локальные имена:', code.co_varnames)
print('константы:', code.co_consts)
локальные имена: ('total', 'bonus')
константы: (None, 10)co_varnames перечислил и параметр, и локальную переменную в порядке появления: сначала total, потом bonus. Константа 10 ушла в co_consts — её в локальных именах нет, это данные, а не имя. Именно такие кортежи компилятор кладёт в кодовый объект ещё до первого вызова функции.
def add(a, b):
return a + b
bytecode = add.__code__.co_code
print('тип байткода:', type(bytecode).__name__)
print('байт в байткоде больше нуля:', len(bytecode) > 0)
тип байткода: bytes байт в байткоде больше нуля: True
Сам байткод — объект типа bytes: кодовые слова и аргументы, упакованные в байты. Именно эти байты лежат в marshal-блоке pyc-файла из второго урока и именно их печатает dis в виде человекочитаемых команд. Длина и содержимое байтов между версиями различаются — поэтому в опыте проверка только «байты есть», а не конкретное число.
dis и программный доступ к инструкциям
Помимо печати, dis умеет отдавать инструкции как объекты: dis.get_instructions возвращает их списком, у каждого есть атрибуты opname, argval, offset. На этом строятся анализаторы кода: поиск запрещённых операций, подсчёт стоимости функции, проверки стиля.
| Инструкция | Что делает | Куда девает стек |
|---|---|---|
| LOAD_FAST | Кладёт локальную переменную | добавляет значение |
| LOAD_CONST | Кладёт константу | добавляет значение |
| STORE_FAST | Сохраняет верхнее значение в переменную | снимает значение |
| BINARY_OP | Бинарная операция над двумя верхними | снимает два, кладёт результат |
| COMPARE_OP | Сравнение двух верхних значений | снимает два, кладёт результат |
| POP_JUMP_IF_FALSE | Условный переход по верхнему значению | снимает значение |
| CALL | Вызов функции с аргументами со стека | снимает аргументы, кладёт результат |
| RETURN_VALUE | Возврат верхнего значения из функции | снимает значение |
Компилятор и его оптимизации уже встретились нам в первом уроке — там же, где мы впервые скомпилировали строку сами. Связка между байткодом и файлами на диске — pyc из второго урока: в marshal-блоке лежат ровно эти байты co_code. А общий знаменатель всех версий байткода — magic-число в заголовке, пятый урок.
import types
source = 'result = 15 + 27\nprint(result)'
code = compile(source, '<проверка>', 'exec')
print('это кодовый объект:', isinstance(code, types.CodeType))
exec(code)
это кодовый объект: True 42
Компилируется и одиночное выражение — режим eval даёт кодовый объект, который eval вычисляет в значение. Передайте такой объект в dis.dis на своей машине: инструкции формулы будут те же LOAD и операции, что и у программ из этого урока:
expr = compile('max(3, 9) * 100', '<формула>', 'eval')
print('результат формулы:', eval(expr))
результат формулы: 900
Полный цикл — компиляция, инспекция, исполнение — в пяти строках. Прогоните этот блок в песочнице, затем передайте code в dis.dis на своей машине: увидите тот же вывод с LOAD_CONST 42, что и в первом уроке, со смещениями и инструкциями своей версии Python.
- dis.dis печатает байткод: строка исходника, смещение, имя команды, аргумент с человекочитаемым значением;
- виртуальная машина стековая: LOAD кладёт значения, операции снимают их со стека, RETURN отдаёт результат;
- вывод dis меняется между версиями Python — опора на модель, а не на заученный список команд;
- стабильные данные о функции — co_name, co_varnames, co_consts; сам байткод — bytes в co_code;
- dis.get_instructions отдаёт инструкции как объекты для программного анализа.
Следующий урок закрывает главный практический вопрос байткода: почему pyc не работает на другой версии Python и при чём тут magic-число.
Сначала предскажи ответ в голове — это главный навык программиста.
def mul(a, b):
return a * b
code = mul.__code__
print(code.co_name, code.co_argcount)
def calc(x):
rate = 10
return x + rate
print(calc.__code__.co_varnames)
import types
code = compile('n = 2\nprint(n * 3)', '<x>', 'exec')
print(isinstance(code, types.CodeType))
exec(code)
1. Что печатает dis.dis(функция)?
2. Что делает инструкция LOAD_FAST?
3. Куда попадают значения, над которыми работает BINARY_OP?
4. Почему вывод dis нельзя заучивать как таблицу умножения?
5. Какой атрибут кодового объекта стабилен между версиями и удобен для анализа?
6. Что возвращает dis.get_instructions(функция)?
7. Какой тип у co_code — самого байткода функции?
Для функции total(price, count): return price * count напечатайте имя кодового объекта, число аргументов и кортеж локальных имён — три строки.
Что такое dis в Python?
Это дизассемблер байткода из стандартной библиотеки: dis.dis переводит байты кодового объекта обратно в человекочитаемые инструкции виртуальной машины. Нужен для понимания, во что компилятор превратил исходник.
Как посмотреть байткод функции?
Одной строкой: import dis; dis.dis(функция). Для кодового объекта из строки — dis.dis(compile('код', '<x>', 'exec')). Атрибуты co_name и co_consts читаются и без dis: это обычные атрибуты __code__.
Урок 1: байткод и VMПочему байткод моей функции отличается от примеров в статьях?
Потому что набор инструкций CPython меняется в каждой минорной версии: меняются имена команд, их аргументы и вид колонок. Вывод, снятый с 3.12, не обязан совпадать с выводом 3.13 — сравнивать надо логику: загрузка, операция, возврат.
Полезно ли читать байткод для оптимизации?
Как диагностика — да: видно, сколько инструкций стоит за выражение и что компилятор свернул сам. Как главный инструмент — нет: в Python оптимизируют алгоритмы и структуры данных, а не отдельные инструкции VM.
Где лежит байткод функции и в каком он виде?
В атрибуте __code__ в поле co_code — это объект bytes: пары кодовое слово + аргумент. В pyc-файле эти же байты лежат в marshal-блоке после 16-байтного заголовка.
Урок 2: устройство pycПонравился урок? Сошлитесь на него
«Байткод — это план работы виртуальной машины: команды могут меняться от версии к версии, но стековая модель исполнения остаётся.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Компиляция · Урок 1
Как Python исполняет код: байткод и виртуальная машина
Питон исполняет программу в два этапа: компилятор переводит исходник в байткод, виртуальная машина выполняет его по инструкциям. Достаём оба этапа на поверхность и вызываем компилятор сами.
Компиляция · Урок 2
pyc-файл: устройство скомпилированного файла Python
pyc — это кодовый объект, упакованный в байты: 16 байт заголовка с magic-числом, временем и размером исходника, дальше marshal-блок с байткодом. Разбираем файл до последнего байта.
Компиляция · Урок 5
Версии Python и magic-число: почему pyc не совместим
Каждая минорная версия CPython получает новое magic-число, и pyc одной версии не грузится другой. Находим число в importlib, сверяем с заголовком pyc и проверяем версию кодом.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Pandas · Урок 2
Чтение файлов в Pandas: read_csv, read_excel и знакомство с данными
read_csv со всеми нужными параметрами и примерами: разделители, кодировки с кириллицей, чтение куска большого файла и первые ошибки, которые вы увидите на практике.
как читать csv файл в pandasчитать sql в pandas
BeautifulSoup / Scrapy · Урок 8
Этичный парсинг: robots.txt, задержки и чтобы не забанили
Парсер собрал три тысячи страниц — а утром каждый запрос возвращает 403. Читаем robots.txt, представляемся по имени, держим вежливый темп и отвечаем на 429 так, чтобы дверь не закрылась навсегда.
как читать robots txt
Компиляция · Урок 15
Декомпиляция pyc в Python: что видно из байткода
pyc не содержит исходного текста, но хранит имена, константы и структуру. Разбираем, что восстановит декомпилятор, что потеряно навсегда и где проходит этическая граница.
python байткод декомпиляция