Це може бути вам корисним - внутрішні програми 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), екскурсія - презентація Пітера Трьогера
першоджерело