Ответ 1
Вы можете найти это полезным - Внутренние элементы 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 и создаете 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]. Это демонстрирует общую технику, которую я использую при изменении исходного кода. Im не знакомы с: работой по подобию. Этот принцип не будет решать все ваши проблемы, но он может определенно облегчить процесс. Поскольку все, что нужно сделать для 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, поскольку ASTs гораздо проще работать с в последующих этапы процесса компиляции.
Итак, мы собираемся посетить Parser/Python.asdl
, который определяет структуру АСТ Python и добавляет AST node для нашего нового оператора 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 script, это своего рода bootstrapping - для создания Python с нуля, Python уже должен быть доступен.
Пока Parser/asdl_c.py
сгенерировал код для управления нашим новым AST node (в файлах Include/Python-ast.h
и Python/Python-ast.c
), нам все равно придется написать код, который преобразует соответствующее дерево синтаксического анализа node в это вручную. Это делается в файле 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
. Наконец, возвращается новый node с именем until
.
Обратите внимание, что мы обращаемся к дереву синтаксического анализа node n
, используя некоторые макросы, такие как NCH
и CHILD
. Это стоит понять - их код находится в Include/node.h
.
Отступление: композиция AST
Я решил создать новый тип АСТ для оператора until
, но на самом деле это необязательно. Я мог бы сэкономить некоторую работу и реализовать новую функциональность, используя состав существующих узлов AST, поскольку:
until condition:
# do stuff
Функционально эквивалентно:
while not condition:
# do stuff
Вместо создания until
node в ast_for_until_stmt
я мог бы создать Not
node с while
node в качестве дочернего. Поскольку компилятор 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
-ping дерева источника не показывает, где оно вызывается. Если это так, остается только один параметр - 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
), который выполняет АСТ так же, как функции генерации кода. Наличие таблицы символов для каждой области помогает компилятору определить некоторую ключевую информацию, например, какие переменные являются глобальными и локальными для области.
Чтобы устранить проблему, мы должны изменить функцию 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) Виртуальная машина, экскурсия - презентация Питера Трегера