Dies kann nützlich sein - Python-Interna: Hinzufügen einer neuen Anweisung zu Python , hier zitiert:
Dieser Artikel ist ein Versuch, die Funktionsweise des Frontends von Python besser zu verstehen. Das bloße Lesen von Dokumentation und Quellcode kann etwas langweilig sein, daher gehe ich hier praktisch vor: Ich werde eine hinzufügenuntil
Python Anweisung .
Die gesamte Codierung für diesen Artikel wurde für den neuesten Py3k-Zweig im Python Mercurial-Repository-Spiegel durchgeführt .
Das until
Aussage
Einige Sprachen, wie Ruby, haben eine until
Aussage, die die Ergänzung zu while
( until num == 0
entspricht while num != 0
) ist. In Ruby kann ich schreiben:
num = 3
until num == 0 do
puts num
num -= 1
end
Und es wird gedruckt:
3
2
1
Daher möchte ich Python eine ähnliche Funktion hinzufügen. Das heißt, schreiben zu können:
num = 3
until num == 0:
print(num)
num -= 1
Ein Exkurs zur Befürwortung der Sprache
In diesem Artikel wird nicht versucht until
, Python eine Anweisung hinzuzufügen. Obwohl ich denke, dass eine solche Aussage den Code klarer machen würde und dieser Artikel zeigt, wie einfach das Hinzufügen ist, respektiere ich Pythons Philosophie des Minimalismus voll und ganz. Alles, was ich hier wirklich versuchen möchte, ist einen Einblick in das Innenleben von Python zu gewinnen.
Ändern der Grammatik
Python verwendet einen benutzerdefinierten Parser-Generator mit dem Namen pgen
. Dies ist ein LL (1) -Parser, der Python-Quellcode in einen Analysebaum konvertiert. Die Eingabe in den Parsergenerator ist die Datei Grammar/Grammar
[1] . Dies ist eine einfache Textdatei, die die Grammatik von Python angibt.
[1] : Von nun an werden Verweise auf Dateien in der Python-Quelle relativ zum Stammverzeichnis des Quellbaums angegeben. Dies ist das Verzeichnis, in dem Sie configure und make zum Erstellen von Python ausführen.
An der Grammatikdatei müssen zwei Änderungen vorgenommen werden. Die erste besteht darin, eine Definition für die until
Anweisung hinzuzufügen . Ich fand heraus, wo die while
Anweisung definiert wurde ( while_stmt
) und fügte until_stmt
unten hinzu [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] : Dies zeigt eine gängige Technik, die ich beim Ändern von Quellcode verwende, mit dem ich nicht vertraut bin: Arbeiten nach Ähnlichkeit . Dieses Prinzip wird nicht alle Ihre Probleme lösen, aber es kann den Prozess definitiv vereinfachen. Denn alles, wofür getan werden while
muss, muss auch getan werdenuntil
, dient es als ziemlich gute Richtlinie.
Beachten Sie, dass ich beschlossen habe, die else
Klausel von meiner Definition von auszuschließen until
, nur um sie ein wenig anders zu machen (und weil ich ehrlich gesagt die nicht magelse
Klausel der Schleifen und nicht denke, dass sie gut zum Zen von Python passt).
Die zweite Änderung ist die Regel zu ändern , um compound_stmt
zu schließen until_stmt
, wie Sie im Snippet oben sehen können. Es ist gleich while_stmt
wieder da.
Wenn Sie laufen make
nach der Änderung Grammar/Grammar
, Ankündigung , dass das pgen
Programm auszuführen ist neu zu generieren Include/graminit.h
und Python/graminit.c
, und dann mehr Dateien bekommen wieder zusammengestellt.
Ändern des AST-Generierungscodes
Nachdem der Python-Parser einen Analysebaum erstellt hat, wird dieser Baum in einen AST konvertiert, da ASTs in nachfolgenden Phasen des Kompilierungsprozesses viel einfacher zu bearbeiten sind.
Wir werden also einen Besuch abstatten, Parser/Python.asdl
der die Struktur der ASTs von Python definiert, und einen AST-Knoten für unsere neue until
Anweisung hinzufügen , ebenfalls direkt unter while
:
| While(expr test, stmt* body, stmt* orelse)
| Until(expr test, stmt* body)
Wenn Sie jetzt ausführen make
, beachten Sie , dass vor dem Kompilieren einer Reihe von Dateien Parser/asdl_c.py
C-Code aus der AST-Definitionsdatei generiert wird. Dies Grammar/Grammar
ist ein weiteres Beispiel für den Python-Quellcode, der eine Minisprache (mit anderen Worten DSL) verwendet, um die Programmierung zu vereinfachen. Da Parser/asdl_c.py
es sich um ein Python-Skript handelt, handelt es sich um eine Art Bootstrapping. Um Python von Grund auf neu zu erstellen, muss Python bereits verfügbar sein.
Während Parser/asdl_c.py
der Code zum Verwalten unseres neu definierten AST-Knotens (in die Dateien Include/Python-ast.h
und Python/Python-ast.c
) generiert wurde , müssen wir den Code, der einen relevanten Analysebaumknoten konvertiert, von Hand schreiben. Dies erfolgt in der Datei Python/ast.c
. Dort ast_for_stmt
konvertiert eine Funktion mit dem Namen Analysebaumknoten für Anweisungen in AST-Knoten. Wiederum while
springen wir , geleitet von unserem alten Freund , direkt in die große Richtung, switch
um zusammengesetzte Anweisungen zu verarbeiten, und fügen eine Klausel hinzu für until_stmt
:
case while_stmt:
return ast_for_while_stmt(c, ch);
case until_stmt:
return ast_for_until_stmt(c, ch);
Jetzt sollten wir implementieren ast_for_until_stmt
. Hier ist es:
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;
}
Auch dies wurde codiert, während das Äquivalent genau betrachtet wurde ast_for_while_stmt
, mit dem Unterschied, dass until
ich mich entschieden habe, die else
Klausel nicht zu unterstützen . Wie erwartet wird der AST rekursiv erstellt, wobei andere AST-Erstellungsfunktionen wie ast_for_expr
der Bedingungsausdruck und ast_for_suite
der Hauptteil der until
Anweisung verwendet werden. Schließlich wird ein neuer Knoten mit dem Namen Until
zurückgegeben.
Beachten Sie, dass wir n
mit einigen Makros wie NCH
und auf den Analysebaumknoten zugreifen CHILD
. Diese sind verständlich - ihr Code ist in Include/node.h
.
Exkurs: AST-Zusammensetzung
Ich habe mich entschieden, einen neuen AST-Typ für die until
Anweisung zu erstellen , aber eigentlich ist dies nicht erforderlich. Ich hätte etwas Arbeit sparen und die neue Funktionalität mithilfe der Zusammensetzung vorhandener AST-Knoten implementieren können, da:
until condition:
# do stuff
Ist funktional äquivalent zu:
while not condition:
# do stuff
Anstatt den Until
Knoten in zu erstellen ast_for_until_stmt
, hätte ich als Kind einen Not
Knoten mit einem While
Knoten erstellen können . Da der AST-Compiler bereits weiß, wie mit diesen Knoten umzugehen ist, können die nächsten Schritte des Prozesses übersprungen werden.
Kompilieren von ASTs in Bytecode
Der nächste Schritt ist das Kompilieren des AST in Python-Bytecode. Die Kompilierung hat ein Zwischenergebnis, das ein CFG (Control Flow Graph) ist, aber da derselbe Code es verarbeitet, werde ich dieses Detail vorerst ignorieren und es für einen anderen Artikel belassen.
Der Code, den wir uns als nächstes ansehen werden, ist Python/compile.c
. In Anlehnung an while
finden wir die Funktion compiler_visit_stmt
, die für das Kompilieren von Anweisungen in Bytecode verantwortlich ist. Wir fügen eine Klausel hinzu für Until
:
case While_kind:
return compiler_while(c, s);
case Until_kind:
return compiler_until(c, s);
Wenn Sie sich fragen, was Until_kind
ist, ist es eine Konstante (eigentlich ein Wert der _stmt_kind
Aufzählung), die automatisch aus der AST-Definitionsdatei in generiert wird Include/Python-ast.h
. Wie auch immer, wir nennen compiler_until
das natürlich noch nicht. Ich werde gleich darauf zurückkommen.
Wenn Sie neugierig sind wie ich, werden Sie feststellen, dass compiler_visit_stmt
das eigenartig ist. Keine Menge von grep
-ping im Quellbaum zeigt, wo er aufgerufen wird. In diesem Fall bleibt nur eine Option übrig - C-Makro-Fu. In der Tat führt uns eine kurze Untersuchung zu dem VISIT
Makro, das definiert ist in Python/compile.c
:
#define VISIT(C, TYPE, V) {\
if (!compiler_visit_ ## TYPE((C), (V))) \
return 0; \
Es wird verwendet , aufzurufen compiler_visit_stmt
in compiler_body
. Zurück zu unserem Geschäft jedoch ...
Wie versprochen, hier ist 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;
}
Ich muss ein Geständnis machen: Dieser Code wurde nicht basierend auf einem tiefen Verständnis des Python-Bytecodes geschrieben. Wie der Rest des Artikels wurde es in Nachahmung der Verwandtschaftsfunktion durchgeführt compiler_while
. Beachten Sie jedoch, dass die Python-VM stapelbasiert ist, und werfen Sie einen Blick in die Dokumentation des dis
Moduls, das eine Liste der Python-Bytecodes enthält mit Beschreibungen enthält, können Sie verstehen, was vor sich geht.
Das war's, wir sind fertig ... oder?
Nachdem wir alle Änderungen vorgenommen und ausgeführt haben make
, können wir den neu kompilierten Python ausführen und unsere neue until
Anweisung ausprobieren :
>>> until num == 0:
... print(num)
... num -= 1
...
3
2
1
Voila, es funktioniert! Sehen wir uns den Bytecode an, der für die neue Anweisung mithilfe des dis
Moduls wie folgt erstellt wurde:
import dis
def myfoo(num):
until num == 0:
print(num)
num -= 1
dis.dis(myfoo)
Hier ist das Ergebnis:
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
Die interessanteste Operation ist Nummer 12: Wenn die Bedingung erfüllt ist, springen wir nach der Schleife zu. Dies ist die richtige Semantik füruntil
. Wenn der Sprung nicht ausgeführt wird, läuft der Schleifenkörper weiter, bis er zu der Bedingung bei Operation 35 zurückspringt.
Ich fühlte mich gut mit meiner Änderung und versuchte dann, die Funktion auszuführen (auszuführen myfoo(3)
), anstatt ihren Bytecode anzuzeigen. Das Ergebnis war weniger als ermutigend:
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 ... das kann nicht gut sein. Also, was ist schief gelaufen?
Der Fall der fehlenden Symboltabelle
Einer der Schritte, die der Python-Compiler beim Kompilieren des AST ausführt, ist das Erstellen einer Symboltabelle für den von ihm kompilierten Code. Der Aufruf von PySymtable_Build
in PyAST_Compile
ruft das Symboltabellenmodul ( Python/symtable.c
) auf, das den AST auf ähnliche Weise wie die Codegenerierungsfunktionen durchläuft. Eine Symboltabelle für jeden Bereich hilft dem Compiler dabei, einige wichtige Informationen herauszufinden, z. B. welche Variablen global und welche lokal für einen Bereich sind.
Um das Problem zu beheben, müssen wir die symtable_visit_stmt
Funktion in ändern Python/symtable.c
und Code für die Behandlung von until
Anweisungen nach dem ähnlichen Code für while
Anweisungen hinzufügen [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] : Übrigens, ohne diesen Code gibt es eine Compiler-Warnung für Python/symtable.c
. Der Compiler stellt fest, dass der Until_kind
Aufzählungswert in der switch-Anweisung von nicht behandelt wird, symtable_visit_stmt
und beschwert sich. Es ist immer wichtig, nach Compiler-Warnungen zu suchen!
Und jetzt sind wir wirklich fertig. Durch das Kompilieren der Quelle nach dieser Änderung wird die Ausführung der myfoo(3)
Arbeit wie erwartet ausgeführt.
Fazit
In diesem Artikel habe ich gezeigt, wie Sie Python eine neue Anweisung hinzufügen. Obwohl der Code des Python-Compilers einiges an Basteln erfordert, war die Änderung nicht schwer zu implementieren, da ich eine ähnliche und vorhandene Anweisung als Richtlinie verwendet habe.
Der Python-Compiler ist ein hochentwickelter Teil der Software, und ich behaupte nicht, ein Experte darin zu sein. Ich interessiere mich jedoch sehr für die Interna von Python und insbesondere für das Front-End. Daher fand ich diese Übung eine sehr nützliche Ergänzung zur theoretischen Untersuchung der Prinzipien und des Quellcodes des Compilers. Es wird als Basis für zukünftige Artikel dienen, die tiefer in den Compiler eindringen.
Verweise
Ich habe einige ausgezeichnete Referenzen für die Konstruktion dieses Artikels verwendet. Hier sind sie in keiner bestimmten Reihenfolge:
- PEP 339: Design des CPython-Compilers - wahrscheinlich die wichtigste und umfassendste offizielle Dokumentation für den Python-Compiler. Da es sehr kurz ist, zeigt es schmerzlich den Mangel an guter Dokumentation der Interna von Python.
- "Python Compiler Internals" - ein Artikel von Thomas Lee
- "Python: Design and Implementation" - eine Präsentation von Guido van Rossum
- Python (2.5) Virtual Machine, Eine Führung - eine Präsentation von Peter Tröger
Originalquelle