Вы можете найти это полезным - Внутренние элементы 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
и не думаю, что хорошо сочетается с дзен питона).
Второе изменение состоит в том, чтобы изменить правило для 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 для оператора 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. Как и в остальной части статьи, это было сделано в имитации функции kin 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
в 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 (2.5) Виртуальная машина, экскурсия - презентация Питера Трегера
оригинальный источник