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ć until
instrukcję do Pythona.
Całe kodowanie tego artykułu zostało wykonane w najnowocześniejszej gałęzi Py3k w lustrze repozytorium Python Mercurial .
until
oświadczenie
Niektóre języki, takie jak Ruby, mają until
instrukcję, która jest uzupełnieniem while
( until num == 0
jest 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 until
instrukcji 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 until
instrukcji. Znalazłem, gdzie while
stwierdzenie zostało zdefiniowane ( while_stmt
) i dodane until_stmt
poniż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ć, while
również musi zostać zrobione until
, służy to jako całkiem dobra wskazówka.
Zauważ, że zdecydowałem się wykluczyć else
klauzulę z mojej definicji until
, tylko po to, aby była trochę inna (i ponieważ szczerze mówiąc nie podoba mi się else
klauzula pętli i nie sądzę, aby dobrze pasowała do Zen w Pythonie).
Druga zmiana polega na zmodyfikowaniu reguły w compound_stmt
celu uwzględnienia until_stmt
, jak widać w powyższym fragmencie. To while_stmt
znowu zaraz potem .
Po uruchomieniu make
po zmodyfikowaniu Grammar/Grammar
, informacja, że pgen
program jest uruchomiony do ponownego generowania Include/graminit.h
i 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.asdl
który definiuje strukturę AST Pythona i dodać węzeł AST dla naszej nowej until
instrukcji, 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.py
jest 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.py
jest 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.py
generowania kodu do zarządzania naszym nowo zdefiniowanym węzłem AST (w plikach Include/Python-ast.h
i 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_stmt
konwertuje węzły drzewa analizy instrukcji na węzły AST. Ponownie, kierując się naszym starym przyjacielem while
, wskakujemy od razu do tematu switch
obsł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ć else
klauzuli. Zgodnie z oczekiwaniami, AST jest tworzony rekurencyjnie, przy użyciu innych funkcji tworzących AST, takich jak ast_for_expr
wyrażenie warunku i ast_for_suite
treść until
instrukcji. Na koniec Until
zwracany jest nowy węzeł o nazwie .
Zauważ, że uzyskujemy dostęp do węzła drzewa parsowania n
za pomocą niektórych makr, takich jak NCH
i CHILD
. Warto je zrozumieć - ich kod jest w Include/node.h
.
Dygresja: skład AST
Zdecydowałem się utworzyć nowy typ AST dla until
instrukcji, 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ć Until
węzeł w ast_for_until_stmt
, mogłem utworzyć Not
węzeł z While
wę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_kind
jest, jest to stała (właściwie wartość _stmt_kind
wyliczenia) automatycznie generowana z pliku definicji AST do Include/Python-ast.h
. W każdym razie nazywamy to, compiler_until
co oczywiście nadal nie istnieje. Zaraz do tego dojdę.
Jeśli jesteś ciekawy jak ja, zauważysz, że compiler_visit_stmt
jest 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 VISIT
makra 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_stmt
w 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 dis
moduł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 make
możemy uruchomić nowo skompilowany Python i wypróbować naszą nową until
instrukcję:
>>> until num == 0:
... print(num)
... num -= 1
...
3
2
1
Voila, to działa! Zobaczmy kod bajtowy utworzony dla nowej instrukcji za pomocą dis
moduł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_Build
in PyAST_Compile
wywoł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_stmt
funkcję w Python/symtable.c
, dodając kod do obsługi until
instrukcji, po podobnym kodzie dla while
instrukcji [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_kind
wartość wyliczenia nie jest obsługiwana w instrukcji switch symtable_visit_stmt
i 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