Це може бути вам корисним - внутрішні програми Python: додавання нового твердження до Python , цитується тут:
Ця стаття - це спроба краще зрозуміти, як працює передній край Python. Просто читання документації та вихідного коду може бути трохи нудним, тому я тут дотримуюся практичного підходу: я збираюся додати until
заяву до Python.
Все кодування цієї статті було зроблено проти передової гілки Py3k у дзеркалі сховища Python Mercurial .
until
заяву
У деяких мовах, як у Ruby, є until
твердження, яке є доповненням while
( until num == 0
еквівалентно while num != 0
). У Рубі я можу написати:
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 надаються відносно кореня дерева джерела, що є каталогом, де ви запускаєте конфігурацію та створюєте для складання 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
пункт циклів і не думаю, що він добре підходить до дзен Python).
Друга зміна - змінити правило для compound_stmt
включення until_stmt
, як ви бачите в фрагменті вище. Це відразу після while_stmt
цього.
Після запуску make
після модифікації Grammar/Grammar
зауважте, що pgen
програма запускається для повторного генерування Include/graminit.h
та Python/graminit.c
, а потім кілька файлів перекомпілюються.
Зміна коду покоління AST
Після того, як аналізатор Python створив дерево розбору, це дерево перетворюється на AST, оскільки з AST набагато простіше працювати з наступними етапами процесу компіляції.
Отже, ми збираємось відвідати, Parser/Python.asdl
яка визначає структуру ASTs 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
-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. Як і решта статті, це було зроблено в імітації родинної compiler_while
функції. Однак уважно прочитавши це, маючи на увазі, що Python VM заснований на стеці, і заглянувши в документацію 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
значення перерахунку не обробляється в операторі перемикача symtable_visit_stmt
і скаржиться. Завжди важливо перевірити наявність попереджень компілятора!
І зараз ми справді робимо. Складання джерела після цієї зміни робить виконання myfoo(3)
роботи таким, як очікувалося.
Висновок
У цій статті я продемонстрував, як додати нову заяву до Python. Хоча це вимагає зовсім трохи повороту в коді компілятора Python, зміни не було важко здійснити, тому що я використовував аналогічний та існуючий вислів як настанову.
Компілятор Python - це складний фрагмент програмного забезпечення, і я не претендую на те, щоб бути експертом у цьому. Однак мене дуже цікавить внутрішня сторінка Python, зокрема його передня частина. Тому я вважаю цю вправу дуже корисним супутником до теоретичного вивчення принципів компілятора та вихідного коду. Він буде слугувати базою для майбутніх статей, які заглиблюються в компілятор.
Список літератури
Для побудови цієї статті я використав кілька чудових посилань. Ось вони, не в певному порядку:
- PEP 339: Дизайн компілятора CPython - мабуть, найважливіший і всеосяжніший фрагмент офіційної документації для компілятора Python. Будучи дуже коротким, він болісно відображає дефіцит хорошої документації про внутрішні програми Python.
- "Внутрішній компілятор Python" - стаття Томаса Лі
- "Python: Дизайн та впровадження" - презентація Гвідо ван Россума
- Віртуальна машина Python (2.5), екскурсія - презентація Пітера Трьогера
першоджерело