Данная серия статей рассчитана на тех, кто умеет писать на python в целом, но плохо представляет как этот язык устроен изнутри. Собственно, как и я три месяца назад.
Небольшой дисклеймер: свой рассказ я буду вести на примере интерпретатора python 2.7. Всё, о чем пойдёт речь далее, можно повторить и на python 3.x с поправкой на некоторые различия в синтаксисе и именование некоторых функций.
Итак, начнём.
Часть I. Слушай Питон, а что у тебя внутри?
Начнём с немного (на самом деле, с сильно) высокоуровневого взгляда на то, что же из себя представляет наша любимая змея. Что происходит, когда вы набираете строку подобную этой в интерактивном интерпретаторе?
>>> a = "hello"
Ваш палец падает на enter и питон инициирует 4 следующих процесса: лексический анализ, парсинг, компиляцию и непосредственно интерпретацию. Лексический анализ – это процесс разбора набранной вами строки кода в определенную последовательность символов, называемых токенами. Далее парсер на основе этих токенов генерирует структуру, которая отображает взаимоотношения между входящими в неё элементами (в данном случае, структура это абстрактное синтаксическое древо или АСД). Далее, используя АСД, компилятор создаёт один или несколько объектных модулей и передаёт их в интерпретатор для непосредственного выполнения.
Я не буду углубляться в темы лексического анализа, парсинга и компиляции, в основном потому, что сам не имею о них ни малейшего представления. Вместо этого, давайте лучше представим, что умные люди сделали всё как надо и данные этапы в питоновском интерпретаторе отрабатывают без ошибок. Представили? Двигаем дальше.
Прежде чем перейти к объектным модулям (или объектам кода, или объектным файлам), следует кое-что прояснить. В данной серии статей мы будем говорить об объектах функций, объектных модулях и байткоде – всё это совершенно разные, хоть и некоторым образом связанные между собой понятия. Хотя нам и необязательно знать, что такое объекты функций для понимания интерпретатора, но я всё же хотел бы остановить на них ваше внимание. Не говоря уже о том, что они попросту крутые.
Итак,
Объекты функций или функции, как объекты
Если это не первая ваша статья о программировании на питоне, вы должны быть наслышаны о неких «объектах функций». Это именно о них люди с умным видом рассуждают в контексте разговоров о «функциях, как объектах первого класса» и «наличии функций первого класса в питоне». Рассмотрим следующий пример:
>>> def foo(a):
... x = 3
... return x + a
...
>>> foo
<function foo at 0x107ef7aa0>
Выражение «функции – это объекты первого класса» означает, что функции – это объекты первого класса, в том смысле, в коем и списки – это объекты, и экземпляр класса MyObject
– объект. И так как foo это объект, он имеет значимость сам по себе, безотносительно вызова его, как функции (то есть, foo
и foo()
— это разные вещи). Мы можем передать foo
в другую функцию в качестве аргумента, можем переназначить её на новое имя (other_function = foo
). С функциями первого класса можно делать, что угодно и они всё стерпят.
Часть 2. Объектные модули
На данном этапе we need to go deeper, чтобы узнать, что объект функции в свою очередь содержит объект кода:
>>> def foo(a):
... x = 3
... return x + a
...
>>> foo
<function foo at 0x107ef7aa0>
>>> foo.func_code
<code object foo at 0x107eeccb0, file "<stdin>", line 1>
Как видно из приведённого листинга, объектный модуль является атрибутом объекта функции (у которого есть и множество других атрибутов, но в данном случае особого интереса они не представляют в силу простоты foo
).
Объектный модуль генерируется питоновским компилятором и далее передаётсся интерпретатору. Модуль содержит всю необходимую для выполнения информацию. Давайте посмотрим на его атрибуты:
>>> dir(foo.func_code)
['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename',
'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals',
'co_stacksize', 'co_varnames']
Их, как видите, немало, поэтому все рассматривать не будем, для примера остановимся на трёх наиболее понятных:
>>> foo.func_code.co_varnames
('a', 'x')
>>> foo.func_code.co_consts
(None, 3)
>>> foo.func_code.co_argcount
1
Атрибуты выглядят довольно интуитивно:
co_varnames
– имена переменных
co_consts
– значения, о которых знает функция
co_argcount
– количество аргументов, которые функция принимает
Всё это весьма познавательно, но выглядит несколько черезчур высокоуровнево для нашей темы, не правда ли? Где же инструкции интерпретатору для непосредственного выполнения нашего модуля? А такие инструкции есть и представлены они байткодом. Последний также является атрибутом объектного модуля:
>>> foo.func_code.co_code 'dx01x00}x01x00|x01x00|x00x00x17S'
Что за неведомая байтовая фигня, спросите вы?
Часть III. Байткод
Вы наверное и сами понимаете, но я, на всякий случай, озвучу – «байткод» и «объект кода» это разные вещи: первый является атрибутом второго, среди многих других (см. часть 2). Атрибут называется co_code
и содержит все необходимые инструкции для выполнения интерпретатором.
Что же из себя представляет этот байткод? Как следует из названия, это просто последовательность байтов. При выводе в консоль выглядит она достаточно бредово, поэтому давайте приведём её к числовой последовательности, пропустив через ord
:
>>> [ord(b) for b in foo.func_code.co_code] [100, 1, 0, 125, 1, 0, 124, 1, 0, 124, 0, 0, 23, 83]
Таким образом мы получили числовое представление питоновского байткода. Интерпретатор пройдётся по каждому байту в последовательности и выполнит связанные с ним инструкции. Обратите внимание, что байткод сам по себе не содержит питоновских объектов, ссылок на объекты и т.п.
Байткод можно попытаться понять открыв файл интерпретатора CPython (ceval.c), но мы этого делать не будем. Точнее будем, но позже. Сейчас же пойдём простым путём и воспользуемся модулем dis
из стандартной библиотеки.
Дизассемблируй это
Дизассемблирование – это перевод байтовой последовательности в нечто более понятное человеческому разуму. Для этой цели в питоне существует модуль dis
, который подробно покажет вам всё, что скрыто. У модуля нет особого применения в продакшн-коде, результаты его работы нужны только вам, не интерпретатору.
Итак, давайте применим dis
и снимем паранжу с нашего объектного модуля. Для этого воспользуемся функцией dis.dis
:
>>> def foo(a):
... x = 3
... return x + a
...
>>> import dis
>>> dis.dis(foo.func_code)
2 0 LOAD_CONST 1 (3)
3 STORE_FAST 1 (x)
3 6 LOAD_FAST 1 (x)
9 LOAD_FAST 0 (a)
12 BINARY_ADD
13 RETURN_VALUE
dis.dis(foo)
, т.е. объект функции передаётся в дизассемблер напрямую. Это сделано для удобства, под капотом dis всё равно находит и анализирует func_code
. В нашем примере мы передаём объект кода явно для лучшего понимания процесса.Числа в первой колонке – это номера строк анализируемых исходников. Вторая колонка отражает смещение команд в байткоде: LOAD_CONST
находится в позиции «0», STORE_FAST
в позиции «3» и т.д. Третья колонка даёт байтовым инструкциям человекопонятные названия. Названия эти нужны только жалким людишкам нам, в интерпретаторе они не используются.
Две последние колонки содержат подробности об аргументах для данной команды, если таковые имеются. Четвёртая колонка отражает позицию аргумента в атрибуте объектного модуля. В нашем примере аргумент инструкции LOAD_CONST
находится на первой позиции атрибута-списка co_consts, аргумент STORE_FAST
– на первой позиции co_varnames
. Наконец, в пятой колонке dis
отражает значение или название соответствующей переменной. Убедимся в сказанном на практике:
>>> foo.func_code.co_consts[1]
3
>>> foo.func_code.co_varnames[1]
'x'
Это также объясняет, почему инструкция STORE_FAST
находится на третьей позиции в байткоде: если где-то в байткоде есть аргумент, следующие два байта будут представлять этот аргумент. Корректная обработка таких ситуаций также ложится на плечи интерпретатора.
BINARY_ADD
– возьмите печеньку за внимательность, но не беспокойтесь раньше времени. Мы вернёмся к этому моменту чуть позже, когда разговор пойдёт о самом интерпретаторе.Как dis
переводит байты (например, 100) в осмысленные имена (например, LOAD_CONST
) и наоборот? Подумайте, как бы вы сами организовали подобную систему? Если у вас появились мысли, вроде «ну, может там есть какой-то список с последовательным определением байтов» или «по-любому словарь с названиями инструкций в качестве ключей и байтами как значениями», поздравляю – вы абсолютно правы. Именно так всё и устроено. Сами определения происходят в файле opcode.py (можно также посмотреть заголовочный файл opcode.h), где вы сможете увидеть ~полторы сотни подобных строк:
def_op('LOAD_CONST', 100) # Index in const list
def_op('BUILD_TUPLE', 102) # Number of tuple items
def_op('BUILD_LIST', 103) # Number of list items
def_op('BUILD_SET', 104) # Number of set items
(Какой-то любитель комментариев заботливо оставил нам пояснения к инструкциям.)
Теперь мы имеем некоторое представление о том, чем является (и чем не является) байткод и как использовать dis
для его анализа. В следующих частях мы рассмотрим, как питон может компилироваться в байткод, оставаясь при этом динамическим ЯП.
Автор: rzhannoy