Вы можете найти это полезным - Внутреннее устройство Python: добавление нового оператора в Python , цитируемого здесь:
Эта статья - попытка лучше понять, как работает интерфейс Python. Просто читать документацию и исходный код может быть немного скучно, поэтому я использую здесь практический подход: я собираюсь добавить until
оператор в Python.
Все кодирование для этой статьи было выполнено с использованием передовой ветки Py3k в зеркале репозитория Python Mercurial .
until
заявление
В некоторых языках, таких как Ruby, есть until
инструкция, которая является дополнением к while
( until num == 0
эквивалентно while num != 0
). На Ruby я могу написать:
num = 3
until num == 0 do
puts num
num -= 1
end
И он напечатает:
3
2
1
Итак, я хочу добавить аналогичную возможность в Python. То есть уметь писать:
num = 3
until num == 0:
print(num)
num -= 1
Экскурсия по языковой защите
В этой статье не делается попытка предложить добавление until
оператора в Python. Хотя я думаю, что такое утверждение сделало бы код более понятным, и эта статья показывает, насколько легко его добавить, я полностью уважаю философию минимализма Python. На самом деле все, что я пытаюсь сделать здесь, это получить некоторое представление о внутренней работе Python.
Изменение грамматики
Python использует собственный генератор парсеров с именем pgen
. Это анализатор LL (1), который преобразует исходный код Python в дерево синтаксического анализа. Входом в генератор парсера является файл Grammar/Grammar
[1] . Это простой текстовый файл, определяющий грамматику Python.
[1] : С этого момента ссылки на файлы в исходном коде Python даются относительно корня исходного дерева, то есть каталога, в котором вы запускаете configure и make для сборки Python.
В файл грамматики необходимо внести две модификации. Первый - добавить определение для until
утверждения. Я нашел, где while
был определен оператор ( while_stmt
), и добавил until_stmt
ниже [2] :
compound_stmt: if_stmt | while_stmt | until_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
while_stmt: 'while' test ':' suite ['else' ':' suite]
until_stmt: 'until' test ':' suite
[2] : Это демонстрирует общий прием, который я использую при изменении исходного кода, с которым я не знаком: работа по подобию . Этот принцип не решит всех ваших проблем, но определенно может облегчить процесс. Поскольку все, что нужно сделать, while
также необходимо сделать until
, это служит довольно хорошим ориентиром.
Обратите внимание, что я решил исключить else
предложение из своего определения until
, просто чтобы сделать его немного другим (и потому, что, честно говоря, мне не нравится else
предложение циклов и я не думаю, что он хорошо сочетается с Zen of Python).
Второе изменение заключается в изменении правила для compound_stmt
включения until_stmt
, как вы можете видеть во фрагменте выше. Это снова сразу после while_stmt
.
При запуске make
после изменения Grammar/Grammar
обратите внимание, что pgen
программа запускается для повторного создания Include/graminit.h
и Python/graminit.c
, а затем несколько файлов повторно компилируются.
Изменение кода генерации AST
После того, как синтаксический анализатор Python создал дерево синтаксического анализа, это дерево преобразуется в AST, поскольку с AST намного проще работать на последующих этапах процесса компиляции.
Итак, мы собираемся посетить страницу, Parser/Python.asdl
которая определяет структуру AST Python и добавить узел AST для нашего нового until
оператора, снова прямо под while
:
| While(expr test, stmt* body, stmt* orelse)
| Until(expr test, stmt* body)
Если вы сейчас запустите make
, обратите внимание, что перед компиляцией кучи файлов Parser/asdl_c.py
запускается для генерации кода C из файла определения AST. Это (как Grammar/Grammar
) еще один пример исходного кода Python, использующего мини-язык (другими словами, DSL) для упрощения программирования. Также обратите внимание, что, поскольку Parser/asdl_c.py
это сценарий Python, это своего рода начальная загрузка - для создания Python с нуля Python уже должен быть доступен.
Пока мы Parser/asdl_c.py
сгенерировали код для управления нашим вновь определенным узлом AST (в файлы Include/Python-ast.h
и Python/Python-ast.c
), нам все еще нужно написать код, который вручную конвертирует в него соответствующий узел дерева синтаксического анализа. Это делается в файле Python/ast.c
. Там функция с именем ast_for_stmt
преобразует узлы дерева синтаксического анализа для операторов в узлы AST. Опять же, руководствуясь нашим старым другом while
, мы сразу переходим switch
к обработке составных операторов и добавляем предложение для until_stmt
:
case while_stmt:
return ast_for_while_stmt(c, ch);
case until_stmt:
return ast_for_until_stmt(c, ch);
Теперь надо реализовать ast_for_until_stmt
. Вот:
static stmt_ty
ast_for_until_stmt(struct compiling *c, const node *n)
{
/* until_stmt: 'until' test ':' suite */
REQ(n, until_stmt);
if (NCH(n) == 4) {
expr_ty expression;
asdl_seq *suite_seq;
expression = ast_for_expr(c, CHILD(n, 1));
if (!expression)
return NULL;
suite_seq = ast_for_suite(c, CHILD(n, 3));
if (!suite_seq)
return NULL;
return Until(expression, suite_seq, LINENO(n), n->n_col_offset, c->c_arena);
}
PyErr_Format(PyExc_SystemError,
"wrong number of tokens for 'until' statement: %d",
NCH(n));
return NULL;
}
Опять же, это было написано при внимательном рассмотрении эквивалента ast_for_while_stmt
, с той разницей, что until
я решил не поддерживать это else
предложение. Как и ожидалось, AST создается рекурсивно с использованием других функций создания AST, таких ast_for_expr
как выражение условия и ast_for_suite
тело until
оператора. Наконец, Until
возвращается новый узел с именем .
Обратите внимание, что мы получаем доступ к узлу дерева синтаксического анализа, n
используя некоторые макросы, такие как NCH
и CHILD
. Их стоит понять - их код находится в формате Include/node.h
.
Лирическое отступление: состав AST
Я решил создать новый тип AST для until
оператора, но на самом деле в этом нет необходимости. Я мог бы сэкономить немного времени и реализовать новую функциональность, используя композицию существующих узлов AST, поскольку:
until condition:
# do stuff
Функционально эквивалентен:
while not condition:
# do stuff
Вместо того, чтобы создавать Until
узел в ast_for_until_stmt
, я мог бы создать Not
узел с While
узлом в качестве дочернего. Поскольку компилятор AST уже знает, как обрабатывать эти узлы, следующие шаги процесса можно пропустить.
Компиляция AST в байт-код
Следующим шагом является компиляция AST в байт-код Python. Компиляция дает промежуточный результат, который является CFG (Control Flow Graph), но, поскольку он обрабатывается тем же кодом, я пока проигнорирую эту деталь и оставлю ее для другой статьи.
Код, который мы рассмотрим дальше, - это Python/compile.c
. Следуя примеру while
, мы находим функцию compiler_visit_stmt
, которая отвечает за компиляцию операторов в байт-код. Мы добавляем пункт для Until
:
case While_kind:
return compiler_while(c, s);
case Until_kind:
return compiler_until(c, s);
Если вам интересно, что Until_kind
это такое, это константа (на самом деле значение _stmt_kind
перечисления), автоматически генерируемая из файла определения AST в Include/Python-ast.h
. Во всяком случае, мы называем то, compiler_until
что, конечно, еще не существует. Я займусь этим на мгновение.
Если вам любопытно, как и мне, вы заметите, что compiler_visit_stmt
это странно. Никакое grep
нажатие на дерево исходных текстов не показывает, где оно вызвано. В таком случае остается только один вариант - C macro-fu. Действительно, небольшое исследование приводит нас к VISIT
макросу, определенному в Python/compile.c
:
#define VISIT(C, TYPE, V) {\
if (!compiler_visit_ ## TYPE((C), (V))) \
return 0; \
Он используется для вызова compiler_visit_stmt
в compiler_body
. Но вернемся к нашему делу ...
Как и было обещано, вот compiler_until
:
static int
compiler_until(struct compiler *c, stmt_ty s)
{
basicblock *loop, *end, *anchor = NULL;
int constant = expr_constant(s->v.Until.test);
if (constant == 1) {
return 1;
}
loop = compiler_new_block(c);
end = compiler_new_block(c);
if (constant == -1) {
anchor = compiler_new_block(c);
if (anchor == NULL)
return 0;
}
if (loop == NULL || end == NULL)
return 0;
ADDOP_JREL(c, SETUP_LOOP, end);
compiler_use_next_block(c, loop);
if (!compiler_push_fblock(c, LOOP, loop))
return 0;
if (constant == -1) {
VISIT(c, expr, s->v.Until.test);
ADDOP_JABS(c, POP_JUMP_IF_TRUE, anchor);
}
VISIT_SEQ(c, stmt, s->v.Until.body);
ADDOP_JABS(c, JUMP_ABSOLUTE, loop);
if (constant == -1) {
compiler_use_next_block(c, anchor);
ADDOP(c, POP_BLOCK);
}
compiler_pop_fblock(c, LOOP, loop);
compiler_use_next_block(c, end);
return 1;
}
Должен признаться: этот код не был написан на основе глубокого понимания байт-кода Python. Как и остальная часть статьи, это было сделано в имитации compiler_while
функции родства . Однако, внимательно прочитав его, помня, что виртуальная машина Python основана на стеке, и заглянув в документацию dis
модуля, в которой есть список байт-кодов Python с описаниями, можно понять, что происходит.
Вот и все, мы закончили ... Не так ли?
После внесения всех изменений и запуска make
мы можем запустить только что скомпилированный Python и попробовать наш новый until
оператор:
>>> until num == 0:
... print(num)
... num -= 1
...
3
2
1
Вуаля, работает! Давайте посмотрим на байт-код, созданный для нового оператора, используя dis
модуль следующим образом:
import dis
def myfoo(num):
until num == 0:
print(num)
num -= 1
dis.dis(myfoo)
Вот результат:
4 0 SETUP_LOOP 36 (to 39)
>> 3 LOAD_FAST 0 (num)
6 LOAD_CONST 1 (0)
9 COMPARE_OP 2 (==)
12 POP_JUMP_IF_TRUE 38
5 15 LOAD_NAME 0 (print)
18 LOAD_FAST 0 (num)
21 CALL_FUNCTION 1
24 POP_TOP
6 25 LOAD_FAST 0 (num)
28 LOAD_CONST 2 (1)
31 INPLACE_SUBTRACT
32 STORE_FAST 0 (num)
35 JUMP_ABSOLUTE 3
>> 38 POP_BLOCK
>> 39 LOAD_CONST 0 (None)
42 RETURN_VALUE
Самая интересная операция - номер 12: если условие истинно, мы переходим к завершению цикла. Это правильная семантика для until
. Если переход не выполняется, тело цикла продолжает работать, пока не вернется к состоянию на этапе 35.
Чувствуя себя довольным своим изменением, я затем попытался запустить функцию (выполнение myfoo(3)
) вместо того, чтобы показывать ее байт-код. Результат был менее чем обнадеживающим:
Traceback (most recent call last):
File "zy.py", line 9, in
myfoo(3)
File "zy.py", line 5, in myfoo
print(num)
SystemError: no locals when loading 'print'
Ого ... это не может быть хорошо. Так что же пошло не так?
Случай с отсутствующей таблицей символов
Одним из шагов, выполняемых компилятором Python при компиляции AST, является создание таблицы символов для кода, который он компилирует. Вызов PySymtable_Build
in PyAST_Compile
вызывает модуль таблицы символов ( Python/symtable.c
), который просматривает AST аналогично функциям генерации кода. Наличие таблицы символов для каждой области помогает компилятору выяснить некоторую ключевую информацию, например, какие переменные являются глобальными, а какие - локальными по отношению к области.
Чтобы решить эту проблему, мы должны изменить symtable_visit_stmt
функцию в Python/symtable.c
, добавив код для обработки until
операторов после аналогичного кода для while
операторов [3] :
case While_kind:
VISIT(st, expr, s->v.While.test);
VISIT_SEQ(st, stmt, s->v.While.body);
if (s->v.While.orelse)
VISIT_SEQ(st, stmt, s->v.While.orelse);
break;
case Until_kind:
VISIT(st, expr, s->v.Until.test);
VISIT_SEQ(st, stmt, s->v.Until.body);
break;
[3] : Кстати, без этого кода компилятор выдает предупреждение для Python/symtable.c
. Компилятор замечает, что Until_kind
значение перечисления не обрабатывается в операторе switch, symtable_visit_stmt
и жалуется. Всегда важно проверять предупреждения компилятора!
И теперь мы действительно закончили. При компиляции исходного кода после этого изменения работа выполняется myfoo(3)
должным образом.
Вывод
В этой статье я продемонстрировал, как добавить новый оператор в Python. Несмотря на то, что потребовалось немного поработать в коде компилятора Python, это изменение было несложно реализовать, потому что я использовал аналогичный существующий оператор в качестве руководства.
Компилятор Python - это сложная часть программного обеспечения, и я не утверждаю, что я в ней эксперт. Однако меня действительно интересует внутреннее устройство Python, и особенно его интерфейс. Поэтому я нашел это упражнение очень полезным дополнением к теоретическому изучению принципов компилятора и исходного кода. Он послужит основой для будущих статей, которые углубятся в компилятор.
Ссылки
Для построения этой статьи я использовал несколько отличных ссылок. Вот они, в произвольном порядке:
- PEP 339: Дизайн компилятора CPython - вероятно, самая важная и полная официальная документация для компилятора Python. Будучи очень коротким, он болезненно демонстрирует нехватку хорошей документации по внутреннему устройству Python.
- «Внутреннее устройство компилятора Python» - статья Томаса Ли
- «Python: дизайн и реализация» - презентация Гвидо ван Россума
- Виртуальная машина Python (2.5), Экскурсия - презентация Питера Трегера
первоисточник