Może ci się to przydać - wewnętrzne elementy Pythona: dodawanie nowej instrukcji do Pythona , cytowane tutaj:
Ten artykuł jest próbą lepszego zrozumienia, jak działa front-end Pythona. Samo przeczytanie dokumentacji i kodu źródłowego może być trochę nudne, więc przyjmuję podejście praktyczne: zamierzam dodać untilinstrukcję do Pythona.
Całe kodowanie tego artykułu zostało wykonane w najnowocześniejszej gałęzi Py3k w lustrze repozytorium Python Mercurial .
untiloświadczenie
Niektóre języki, takie jak Ruby, mają untilinstrukcję, która jest uzupełnieniem while( until num == 0jest odpowiednikiem while num != 0). W Rubim mogę napisać:
num = 3
until num == 0 do
puts num
num -= 1
end
I wydrukuje:
3
2
1
Chcę więc dodać podobną możliwość do Pythona. Oznacza to, że można pisać:
num = 3
until num == 0:
print(num)
num -= 1
Dygresja na rzecz języka
Ten artykuł nie jest próbą sugerowania dodania untilinstrukcji do Pythona. Chociaż myślę, że takie stwierdzenie uczyniłoby jakiś kod bardziej przejrzystym, a ten artykuł pokazuje, jak łatwo jest go dodać, całkowicie szanuję filozofię minimalizmu Pythona. Jedyne, co próbuję tutaj zrobić, to uzyskać wgląd w wewnętrzne działanie Pythona.
Modyfikacja gramatyki
Python używa niestandardowego generatora parserów o nazwie pgen. To jest parser LL (1), który konwertuje kod źródłowy Pythona na drzewo parsowania. Dane wejściowe do generatora parsera to plik Grammar/Grammar[1] . To jest prosty plik tekstowy, który określa gramatykę języka Python.
[1] : Odtąd odniesienia do plików w źródle Pythona są podawane względnie do katalogu głównego drzewa źródłowego, czyli katalogu, w którym uruchamiasz configure i make, aby zbudować Python.
W pliku gramatyki należy wprowadzić dwie modyfikacje. Pierwszą jest dodanie definicji untilinstrukcji. Znalazłem, gdzie whilestwierdzenie zostało zdefiniowane ( while_stmt) i dodane until_stmtponiżej [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] : To pokazuje powszechną technikę, której używam podczas modyfikowania kodu źródłowego, którego nie znam: praca według podobieństwa . Ta zasada nie rozwiąże wszystkich problemów, ale zdecydowanie może ułatwić proces. Ponieważ wszystko, co trzeba zrobić, whilerównież musi zostać zrobione until, służy to jako całkiem dobra wskazówka.
Zauważ, że zdecydowałem się wykluczyć elseklauzulę z mojej definicji until, tylko po to, aby była trochę inna (i ponieważ szczerze mówiąc nie podoba mi się elseklauzula pętli i nie sądzę, aby dobrze pasowała do Zen w Pythonie).
Druga zmiana polega na zmodyfikowaniu reguły w compound_stmtcelu uwzględnienia until_stmt, jak widać w powyższym fragmencie. To while_stmtznowu zaraz potem .
Po uruchomieniu makepo zmodyfikowaniu Grammar/Grammar, informacja, że pgenprogram jest uruchomiony do ponownego generowania Include/graminit.hi Python/graminit.c, a następnie kilka plików uzyskać ponownie skompilowany.
Modyfikacja kodu generacji AST
Po utworzeniu przez parsera Pythona drzewa parsowania, drzewo to jest konwertowane na AST, ponieważ AST jest znacznie prostszy w pracy na kolejnych etapach procesu kompilacji.
Więc zamierzamy odwiedzić, Parser/Python.asdlktóry definiuje strukturę AST Pythona i dodać węzeł AST dla naszej nowej untilinstrukcji, ponownie tuż pod while:
| While(expr test, stmt* body, stmt* orelse)
| Until(expr test, stmt* body)
Jeśli teraz uruchomisz make, zwróć uwagę, że przed skompilowaniem wielu plików Parser/asdl_c.pyjest uruchamiany w celu wygenerowania kodu C z pliku definicji AST. To (podobnie Grammar/Grammar) jest kolejnym przykładem kodu źródłowego Pythona używającego minijęzyka (innymi słowy DSL) w celu uproszczenia programowania. Zauważ również, że ponieważ Parser/asdl_c.pyjest to skrypt w Pythonie, jest to rodzaj ładowania początkowego - aby zbudować Pythona od podstaw, Python musi już być dostępny.
Podczas Parser/asdl_c.pygenerowania kodu do zarządzania naszym nowo zdefiniowanym węzłem AST (w plikach Include/Python-ast.hi Python/Python-ast.c), nadal musimy ręcznie napisać kod, który konwertuje do niego odpowiedni węzeł drzewa parsowania. Odbywa się to w pliku Python/ast.c. Tam funkcja o nazwie ast_for_stmtkonwertuje węzły drzewa analizy instrukcji na węzły AST. Ponownie, kierując się naszym starym przyjacielem while, wskakujemy od razu do tematu switchobsługi instrukcji złożonych i dodajemy klauzulę dla until_stmt:
case while_stmt:
return ast_for_while_stmt(c, ch);
case until_stmt:
return ast_for_until_stmt(c, ch);
Teraz powinniśmy wdrożyć ast_for_until_stmt. Oto ona:
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;
}
Ponownie, zostało to zakodowane podczas dokładnego przyglądania się odpowiednikowi ast_for_while_stmt, z tą różnicą, untilże zdecydowałem się nie popierać elseklauzuli. Zgodnie z oczekiwaniami, AST jest tworzony rekurencyjnie, przy użyciu innych funkcji tworzących AST, takich jak ast_for_exprwyrażenie warunku i ast_for_suitetreść untilinstrukcji. Na koniec Untilzwracany jest nowy węzeł o nazwie .
Zauważ, że uzyskujemy dostęp do węzła drzewa parsowania nza pomocą niektórych makr, takich jak NCHi CHILD. Warto je zrozumieć - ich kod jest w Include/node.h.
Dygresja: skład AST
Zdecydowałem się utworzyć nowy typ AST dla untilinstrukcji, ale w rzeczywistości nie jest to konieczne. Mogłem zaoszczędzić trochę pracy i zaimplementować nową funkcjonalność przy użyciu kompozycji istniejących węzłów AST, ponieważ:
until condition:
# do stuff
Jest funkcjonalnie równoważne z:
while not condition:
# do stuff
Zamiast tworzyć Untilwęzeł w ast_for_until_stmt, mogłem utworzyć Notwęzeł z Whilewęzłem jako dziecko. Ponieważ kompilator AST już wie, jak obsługiwać te węzły, można pominąć kolejne kroki procesu.
Kompilowanie AST do kodu bajtowego
Następnym krokiem jest skompilowanie AST do kodu bajtowego Pythona. Kompilacja ma pośredni wynik, którym jest CFG (Control Flow Graph), ale ponieważ obsługuje go ten sam kod, na razie zignoruję ten szczegół i zostawię go w innym artykule.
Kod, któremu przyjrzymy się dalej, to Python/compile.c. Idąc za tropem while, znajdujemy funkcję compiler_visit_stmt, która jest odpowiedzialna za kompilowanie instrukcji do kodu bajtowego. Dodajemy klauzulę na Until:
case While_kind:
return compiler_while(c, s);
case Until_kind:
return compiler_until(c, s);
Jeśli zastanawiasz się, co to Until_kindjest, jest to stała (właściwie wartość _stmt_kindwyliczenia) automatycznie generowana z pliku definicji AST do Include/Python-ast.h. W każdym razie nazywamy to, compiler_untilco oczywiście nadal nie istnieje. Zaraz do tego dojdę.
Jeśli jesteś ciekawy jak ja, zauważysz, że compiler_visit_stmtjest to dziwne. Żadna ilość grep-ping drzewa źródłowego nie ujawnia, gdzie jest wywołane. W takim przypadku pozostaje tylko jedna opcja - C makro-fu. Rzeczywiście, krótkie śledztwo prowadzi nas do VISITmakra zdefiniowanego w Python/compile.c:
#define VISIT(C, TYPE, V) {\
if (!compiler_visit_ ## TYPE((C), (V))) \
return 0; \
Jest używany do wywoływania compiler_visit_stmtw compiler_body. Wracając jednak do naszej działalności ...
Zgodnie z obietnicą, oto 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;
}
Muszę się przyznać: ten kod nie został napisany w oparciu o głębokie zrozumienie kodu bajtowego Pythona. Podobnie jak reszta artykułu, zrobiono to naśladując funkcję rodziny compiler_while. Czytając go uważnie, pamiętając jednak, że maszyna wirtualna Pythona jest oparta na stosie, i zaglądając do dokumentacji dismodułu, która zawiera listę kodów bajtowych Pythona wraz z opisami, można zrozumieć, co się dzieje.
To wszystko, skończyliśmy ... prawda?
Po wprowadzeniu wszystkich zmian i uruchomieniu makemożemy uruchomić nowo skompilowany Python i wypróbować naszą nową untilinstrukcję:
>>> until num == 0:
... print(num)
... num -= 1
...
3
2
1
Voila, to działa! Zobaczmy kod bajtowy utworzony dla nowej instrukcji za pomocą dismodułu w następujący sposób:
import dis
def myfoo(num):
until num == 0:
print(num)
num -= 1
dis.dis(myfoo)
Oto wynik:
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
Najciekawszą operacją jest numer 12: jeśli warunek jest prawdziwy, przeskakujemy za pętlą. To jest poprawna semantyka dla until. Jeśli skok nie zostanie wykonany, treść pętli działa, dopóki nie wróci do stanu z operacji 35.
Czując się dobrze po zmianie, spróbowałem uruchomić funkcję (wykonać myfoo(3)) zamiast wyświetlać jej kod bajtowy. Wynik był mniej niż zachęcający:
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'
Whoa ... to nie może być dobre. Więc co poszło nie tak?
Przypadek brakującej tablicy symboli
Jednym z kroków, które kompilator Pythona wykonuje podczas kompilowania AST, jest utworzenie tablicy symboli dla kompilowanego kodu. Wywołanie PySymtable_Buildin PyAST_Compilewywołuje moduł tablicy symboli ( Python/symtable.c), który porusza się po AST w sposób podobny do funkcji generowania kodu. Posiadanie tabeli symboli dla każdego zakresu pomaga kompilatorowi w ustaleniu niektórych kluczowych informacji, takich jak, które zmienne są globalne, a które lokalne.
Aby rozwiązać problem, musimy zmodyfikować symtable_visit_stmtfunkcję w Python/symtable.c, dodając kod do obsługi untilinstrukcji, po podobnym kodzie dla whileinstrukcji [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] : Nawiasem mówiąc, bez tego kodu jest ostrzeżenie kompilatora dla Python/symtable.c. Kompilator zauważa, że Until_kindwartość wyliczenia nie jest obsługiwana w instrukcji switch symtable_visit_stmti narzeka. Zawsze ważne jest, aby sprawdzić ostrzeżenia kompilatora!
A teraz naprawdę skończyliśmy. Kompilowanie źródła po tej zmianie powoduje wykonanie myfoo(3)pracy zgodnie z oczekiwaniami.
Wniosek
W tym artykule pokazałem, jak dodać nową instrukcję do Pythona. Choć wymagała sporo majsterkowania w kodzie kompilatora Pythona, zmiana nie była trudna do zaimplementowania, ponieważ jako wskazówkę użyłem podobnej i istniejącej instrukcji.
Kompilator Pythona to wyrafinowany kawałek oprogramowania i nie twierdzę, że jestem w nim ekspertem. Jednak naprawdę interesują mnie wewnętrzne elementy Pythona, a zwłaszcza jego interfejs. Dlatego uważam, że to ćwiczenie jest bardzo przydatnym towarzyszem teoretycznego badania zasad kompilatora i kodu źródłowego. Posłuży jako podstawa dla przyszłych artykułów, które zagłębią się w kompilator.
Bibliografia
Przy konstrukcji tego artykułu wykorzystałem kilka doskonałych odniesień. Oto one, w przypadkowej kolejności:
- PEP 339: Projekt kompilatora CPythona - prawdopodobnie najważniejszy i najbardziej wszechstronny fragment oficjalnej dokumentacji kompilatora Pythona. Będąc bardzo krótkim, boleśnie pokazuje niedobór dobrej dokumentacji wewnętrznych elementów Pythona.
- „Python Compiler Internals” - artykuł Thomasa Lee
- „Python: Design and Implementation” - prezentacja Guido van Rossuma
- Maszyna wirtualna Python (2.5), wycieczka z przewodnikiem - prezentacja Petera Trögera
Pierwotnym źródłem