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

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

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

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__. Вот её байткод:

байткод функции discount
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
Дизассемблер не запускается в песочнице: вывод зависит от версии Python. Показан вывод CPython 3.13 — в 3.12 колонка смещений другая, COMPARE_OP печатается как 92 (>=), а переход как to 22. Читайте блок как схему, а не как эталон.

Разбираем колонки слева направо. Первая — номер строки исходника: инструкции второй строки программы все помечены двойкой. Вторая — смещение инструкции внутри байткода и метки переходов вроде L1. Третья — имя команды, четвёртая — её аргумент; в скобках dis печатает человекочитаемое значение аргумента: не абстрактный индекс 0, а конкретное (price).

КолонкаСмыслПример из вывода
СтрокаНомер строки исходника2, 3, 4
Смещение и меткаПозиция инструкции в байткоде5 (to L1)
Имя командыЧто делает VMLOAD_FAST, COMPARE_OP
АргументДанные команды0 (price), 1 (100)

Стековая машина: как VM исполняет команды

Инструкции работают не с переменными напрямую, а со стеком — последним хранилищем «положил сверху, взял сверху». LOAD_FAST кладёт значение локальной переменной на стек, COMPARE_OP снимает два верхних значения и сравнивает их, POP_JUMP_IF_FALSE снимает результат и при нуле прыгает на метку. Проследим за сложением из простой функции:

байткод функции add
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
Дизассемблер не запускается в песочнице: вывод зависит от версии Python. Показан CPython 3.13, где пара загрузок склеена в LOAD_FAST_LOAD_FAST; на CPython 3.12 будет две отдельные инструкции LOAD_FAST с явными смещениями. Сама логика — «загрузили, сложили, вернули» — одинакова.

Три команды на всю функцию: загрузить значения, выполнить сложение, вернуть результат. Сложение на стеке выглядит так: LOAD кладёт a и b, BINARY_OP снимает оба и кладёт обратно сумму, RETURN_VALUE снимает верхнее значение и отдаёт его вызывающему. Стековая модель объясняет порядок во всех инструкциях: всё, что нужно операции, сначала оказывается на стеке. Байткод — это план работы виртуальной машины: команды могут меняться от версии к версии, но стековая модель исполнения остаётся.

Что байткод говорит о коде

Чтение dis отвечает на вопросы, которые не видны в исходнике. Что компилятор свернул? Что попадает в константы? Сколько команд стоит за выражение? Всё это лежит в кодовом объекте рядом с байткодом — и читается обычными атрибутами:

константы и имена функции discount
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)
Проверь себя
0 / 7

1. Что печатает dis.dis(функция)?

2. Что делает инструкция LOAD_FAST?

3. Куда попадают значения, над которыми работает BINARY_OP?

4. Почему вывод dis нельзя заучивать как таблицу умножения?

5. Какой атрибут кодового объекта стабилен между версиями и удобен для анализа?

6. Что возвращает dis.get_instructions(функция)?

7. Какой тип у co_code — самого байткода функции?

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

Для функции total(price, count): return price * count напечатайте имя кодового объекта, число аргументов и кортеж локальных имён — три строки.

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

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

TelegramVK

Что дальше

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

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

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