Интерпретатор Python: о чём думает змея? (часть I-III)

    image

    От переводчика
    Весьма вольный перевод серии из трёх статей об устройстве питоновского интерпретатора. Автор занимается разработкой собственного велосипеда по этой теме и решил поделиться знаниями, появившимися в процессе. Посмотрим, что у него из этого получилось.


    Данная серия статей рассчитана на тех, кто умеет писать на 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). С функциями первого класса можно делать, что угодно и они всё стерпят.

    Часть II. Объектные модули



    На данном этапе 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 'd\x01\x00}\x01\x00|\x01\x00|\x00\x00\x17S'
    


    Что за неведомая байтовая фигня, спросите вы?

    Часть 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 для его анализа. В следующих частях мы рассмотрим, как питон может компилироваться в байткод, оставаясь при этом динамическим ЯП.
    Метки:
    • +33
    • 28,3k
    • 6
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 6

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.