In der virtuellen Python-Maschine. Teil 1



Hallo alle zusammen. Ich beschloss schließlich herauszufinden, wie der Python-Interpreter funktioniert. Zu diesem Zweck begann er, ein Artikelbuch zu studieren und konzipierte es gleichzeitig, um es ins Russische zu übersetzen. Tatsache ist, dass Übersetzungen es Ihnen nicht erlauben, einen unverständlichen Satz zu übersehen, und die Qualität der Assimilation des Materials steigt.Ich entschuldige mich im Voraus für mögliche Ungenauigkeiten. Ich versuche immer, so richtig wie möglich zu übersetzen, aber eines der Hauptprobleme: Einige Begriffe werden im russischen Äquivalent einfach nicht erwähnt.

Übersetzungshinweis
Python , «code object», ( ) . , .

— Python, - , : , , ( ! ) , , - ( ) .. — () - str (, Python3, bytes).

Einführung


Die Programmiersprache Python gibt es schon seit geraumer Zeit. Die Entwicklung der ersten Version wurde 1989 von Guido Van Rossum begonnen. Seitdem ist die Sprache gewachsen und zu einer der beliebtesten geworden. Python wird in verschiedenen Anwendungen verwendet: von grafischen Oberflächen bis hin zu Datenanalyseanwendungen.

Der Zweck dieses Artikels besteht darin, einen Blick hinter die Kulissen des Interpreters zu werfen und einen konzeptionellen Überblick über die Ausführung eines in Python geschriebenen Programms zu geben. CPython wird in diesem Artikel berücksichtigt, da es zum Zeitpunkt des Schreibens die beliebteste und grundlegendste Python-Implementierung ist.

Python und CPython werden in diesem Text als Synonyme verwendet, aber jede Erwähnung von Python bedeutet CPython (die in C implementierte Python-Version). Andere Implementierungen umfassen PyPy (Python, das in einer begrenzten Teilmenge von Python implementiert ist), Jython (Implementierung auf der Java Virtual Machine) usw.

Ich mag es, die Ausführung eines Python-Programms in zwei oder drei Hauptschritte (unten aufgeführt) zu unterteilen, je nachdem, wie der Interpreter aufgerufen wird. Diese Schritte werden in diesem Artikel in unterschiedlichem Maße behandelt:

  1. Initialisierung - In diesem Schritt werden die verschiedenen Datenstrukturen eingerichtet, die für den Python-Prozess erforderlich sind. Dies geschieht höchstwahrscheinlich, wenn das Programm im nicht interaktiven Modus über die Shell des Interpreters ausgeführt wird.
  2. — , : , , .
  3. — .

Der Mechanismus zum Generieren von "Parsing" -Bäumen sowie von abstrakten Syntaxbäumen (ASD) ist sprachunabhängig. Daher werden wir dieses Thema nicht sehr ausführlich behandeln, da die in Python verwendeten Methoden den Methoden anderer Programmiersprachen ähnlich sind. Andererseits ist der Prozess der Erstellung von Symboltabellen und Codeobjekten aus ADS in Python spezifischer und verdient daher besondere Aufmerksamkeit. Außerdem wird die Interpretation kompilierter Codeobjekte und aller anderen Datenstrukturen erörtert. Zu den von uns behandelten Themen gehören unter anderem: das Erstellen von Symboltabellen und das Erstellen von Codeobjekten, Python-Objekten, Frame-Objekten, Code-Objekten, Funktionsobjekten, Opcodes, Interpreter-Schleifen, Generatoren und Benutzerklassen.

Dieses Material richtet sich an alle, die lernen möchten, wie die virtuelle CPython-Maschine funktioniert. Es wird davon ausgegangen, dass der Benutzer bereits mit Python vertraut ist und die Grundlagen der Sprache versteht. Wenn wir die Struktur einer virtuellen Maschine untersuchen, werden wir auf eine erhebliche Menge an C-Code stoßen, so dass es für einen Benutzer, der ein elementares Verständnis der C-Sprache hat, einfacher ist, das Material zu verstehen. Und im Grunde genommen das, was Sie brauchen, um sich mit diesem Material vertraut zu machen: der Wunsch, mehr über die virtuelle CPython-Maschine zu erfahren.

Dieser Artikel ist eine erweiterte Version persönlicher Notizen, die im Studium der internen Arbeit des Dolmetschers gemacht wurden. In PyCon-Videos , Schulvorträgen und diesem Blog steckt viel Qualitätsmaterial .. Ohne diese fantastischen Wissensquellen wäre meine Arbeit nicht abgeschlossen worden.

Am Ende dieses Buches wird der Leser in der Lage sein, die Feinheiten der Ausführung Ihres Programms durch den Python-Interpreter zu verstehen. Dies umfasst die verschiedenen Phasen der Programmausführung und Datenstrukturen, die im Programm kritisch sind. Zunächst sehen wir aus der Vogelperspektive, was passiert, wenn ein triviales Programm ausgeführt wird und der Name des Moduls in der Befehlszeile an den Interpreter übergeben wird. Ausführbarer CPython-Code kann gemäß dem Python-Entwicklerhandbuch aus dem Quellcode installiert werden .

Dieses Buch verwendet die Python 3-Version.

30.000 Fuß Ansicht


In diesem Kapitel wird erläutert, wie der Interpreter ein Python-Programm ausführt. In den folgenden Kapiteln werden wir die verschiedenen Teile dieses „Puzzles“ untersuchen und eine detailliertere Beschreibung jedes Teils liefern. Unabhängig von der Komplexität eines in Python geschriebenen Programms ist dieser Prozess immer der gleiche. Die ausgezeichnete Erklärung, die Yaniv Aknin in seiner Artikelserie über Python Internal gegeben hat , bildet das Thema für unsere Diskussion.

Das Quellmodul test.py kann über die Befehlszeile ausgeführt werden (wenn es als Argument in Form von $ python test.py an das Python-Interpreter-Programm übergeben wird). Dies ist nur eine Möglichkeit, die ausführbare Python-Datei aufzurufen. Wir können auch einen interaktiven Interpreter starten, Zeilen einer Datei als Code ausführen usw. Aber diese und andere Methoden interessieren uns nicht. Die Übertragung des Moduls als Argument (innerhalb der Befehlszeile) in die ausführbare Datei (Abbildung 2.1) spiegelt am besten den Ablauf verschiedener Aktionen wider, die an der tatsächlichen Ausführung des Codes beteiligt sind.


Abbildung 2.1: Stream zur Laufzeit

Die ausführbare Python-Datei ist ein reguläres C-Programm. Wenn sie aufgerufen wird, treten also Prozesse auf, die denen ähneln, die beispielsweise im Linux-Kernel oder im einfachen Hallo-Welt-Programm vorhanden sind. Nehmen Sie sich eine Minute Zeit, um zu verstehen: Die ausführbare Python-Datei ist nur ein weiteres Programm, das Ihr eigenes startet. Solche "Beziehungen" bestehen zwischen der C-Sprache und dem Assembler (oder llvm). Der Standardinitialisierungsprozess (der von der Plattform abhängt, auf der die Ausführung stattfindet) beginnt, wenn die ausführbare Python-Datei mit dem Modulnamen als Argument aufgerufen wird.

In diesem Artikel wird davon ausgegangen, dass Sie ein Unix-basiertes Betriebssystem verwenden. Daher können einige Funktionen unter Windows variieren.

Die C- Sprache beim Start führt all ihre "Magie" der Initialisierung aus - lädt Bibliotheken, überprüft / setzt Umgebungsvariablen, und danach wird die Hauptmethode der ausführbaren Python-Datei wie jedes andere C-Programm gestartet. Die ausführbare Hauptdatei von Python befindet sich in ./Programs/python.c und führt eine Initialisierung durch (z. B. das Erstellen von Kopien von Programmbefehlszeilenargumenten, die an das Modul übergeben wurden). Die Hauptfunktion ruft dann die Py_Main- Funktion in ./Modules/main.c auf . Es verarbeitet den Initialisierungsprozess des Interpreters: analysiert die Befehlszeilenargumente, setzt Flags, liest Umgebungsvariablen, führt Hooks aus, randomisiert Hash-Funktionen usw. Auch genanntPy_Initialize aus pylifecycle.c , das die Initialisierung von Interpreter- und Stream- Statusdatenstrukturen übernimmt, sind zwei sehr wichtige Datenstrukturen.

Die Prüfung von Deklarationen von Interpreterdatenstrukturen und Stream-Zuständen macht deutlich, warum sie benötigt werden. Der Status des Interpreters und des Streams sind nur Strukturen mit Zeigern auf Felder, die die zur Ausführung des Programms erforderlichen Informationen enthalten. Interpreter- Statusdaten werden über typedef erstellt (stellen Sie sich dieses Schlüsselwort in C als Typdefinition vor, obwohl dies nicht ganz richtig ist). Der Code für diese Struktur ist in Listing 2.1 dargestellt.

 1     typedef struct _is {
 2 
 3         struct _is *next;
 4         struct _ts *tstate_head;
 5 
 6         PyObject *modules;
 7         PyObject *modules_by_index;
 8         PyObject *sysdict;
 9         PyObject *builtins;
10         PyObject *importlib;
11 
12         PyObject *codec_search_path;
13         PyObject *codec_search_cache;
14         PyObject *codec_error_registry;
15         int codecs_initialized;
16         int fscodec_initialized;
17 
18         PyObject *builtins_copy;
19     } PyInterpreterState;

Codeauflistung 2.1: Datenstruktur des Interpreter-Status

Jeder, der die Programmiersprache Python lange Zeit verwendet hat, kann mehrere in dieser Struktur erwähnte Felder erkennen (sysdict, builtins, codec).

  1. Das Feld * next verweist auf eine andere Instanz des Interpreters, da innerhalb desselben Prozesses mehrere Python-Interpreter vorhanden sein können.
  2. Das Feld * tstate_head gibt den Hauptausführungsthread an (wenn das Programm über mehrere Threads verfügt, ist der Interpreter für alle vom Programm erstellten Threads gleich). Wir werden dies in Kürze genauer besprechen.
  3. modules, modules_by_index, sysdict, builtins und importlib sprechen für sich. Alle von ihnen sind als Instanzen von PyObject definiert , dem Stammtyp für alle Objekte in der virtuellen Python-Maschine. Python-Objekte werden in den folgenden Kapiteln ausführlicher erläutert.
  4. Die Felder für Codec * enthalten Informationen, die beim Herunterladen von Codierungen hilfreich sind. Dies ist sehr wichtig für die Dekodierung von Bytes.

Die Programmausführung muss in einem Thread erfolgen. Die Statusstruktur des Streams enthält alle Informationen, die der Stream benötigt, um ein Codeobjekt auszuführen. Ein Teil der Stream-Datenstruktur ist in Listing 2.2 dargestellt.

 1     typedef struct _ts {
 2         struct _ts *prev;
 3         struct _ts *next;
 4         PyInterpreterState *interp;
 5 
 6         struct _frame *frame;
 7         int recursion_depth;
 8         char overflowed; 
 9                         
10         char recursion_critical; 
11         int tracing;
12         int use_tracing;
13 
14         Py_tracefunc c_profilefunc;
15         Py_tracefunc c_tracefunc;
16         PyObject *c_profileobj;
17         PyObject *c_traceobj;
18 
19         PyObject *curexc_type;
20         PyObject *curexc_value;
21         PyObject *curexc_traceback;
22 
23         PyObject *exc_type;
24         PyObject *exc_value;
25         PyObject *exc_traceback;
26 
27         PyObject *dict;  /* Stores per-thread state */
28         int gilstate_counter;
29 
30         ... 
31     } PyThreadState;

Listing 2.2: Teil der Datenstruktur des Stream-Status

Die Interpreter-Datenstrukturen und Stream-Status werden in den folgenden Kapiteln ausführlicher erläutert. Der Initialisierungsprozess richtet auch Importmechanismen sowie elementares Standard ein.

Nach Abschluss aller Initialisierungen ruft Py_Main die Funktion run_file auf (ebenfalls im Modul main.c). Das Folgende ist eine Reihe von Funktionsaufrufen: PyRun_AnyFileExFlags -> PyRun_SimpleFileExFlags -> PyRun_FileExFlags -> PyParser_ASTFromFileObject. PyRun_SimpleFileExFlagsErstellt den __main__-Namespace, in dem der Inhalt der Datei ausgeführt wird. Es wird auch geprüft, ob die pyc-Version der Datei vorhanden ist (die pyc-Datei ist eine einfache Datei, die eine bereits kompilierte Version des Quellcodes enthält). Wenn eine pyc-Version vorhanden ist, wird versucht, sie als Binärdatei zu lesen und anschließend auszuführen. Wenn die pyc-Datei fehlt, wird PyRun_FileExFlags usw. aufgerufen. Die Funktion PyParser_ASTFromFileObject ruft PyParser_ParseFileObject auf , das den Inhalt des Moduls liest und daraus Analysebäume erstellt . Anschließend wird der erstellte Baum an PyParser_ASTFromNodeObject übergeben , das daraus einen abstrakten Syntaxbaum erstellt.

, Py_INCREF Py_DECREF. , . CPython : , , Py_INCREF. , , Py_DECREF.

AST wird generiert, wenn run_mod aufgerufen wird . Diese Funktion ruft PyAST_CompileObject auf , das Codeobjekte aus AST erstellt. Beachten Sie, dass der während des PyAST_CompileObject-Aufrufs generierte Bytecode durch den einfachen Gucklochoptimierer geleitet wird , der vor dem Erstellen von Codeobjekten eine geringe Optimierung des generierten Bytecodes durchführt. Die Funktion run_mod wendet dann die Funktion PyEval_EvalCode aus der Datei ceval.c auf das Codeobjekt an. Dies führt zu einer weiteren Reihe von Funktionsaufrufen: PyEval_EvalCode -> PyEval_EvalCode -> _PyEval_EvalCodeWithName -> _PyEval_EvalFrameEx. Das Codeobjekt wird in der einen oder anderen Form als Argument an die meisten dieser Funktionen übergeben. _PyEval_EvalFrameEx- Dies ist eine normale Interpreter-Schleife, die die Ausführung von Codeobjekten übernimmt. Es wird jedoch nicht nur mit dem Codeobjekt als Argument aufgerufen, sondern auch mit dem Rahmenobjekt, das als Attribut ein Feld hat, das auf das Codeobjekt verweist. Dieser Frame stellt den Kontext für die Ausführung des Codeobjekts bereit. In einfachen Worten: Die Interpreterschleife liest kontinuierlich den nächsten Befehl, der durch den Befehlszähler angezeigt wird, aus dem Befehlsarray. Dann führt es diese Anweisung aus: Es fügt dem Prozess Objekte hinzu oder entfernt sie aus dem Wertestapel, bis es in das Array der auszuführenden Anweisungen entleert wird (nun, oder es passiert etwas Außergewöhnliches, das die Schleife stört).

Python bietet eine Reihe von Funktionen, mit denen Sie echte Codeobjekte untersuchen können. Beispielsweise kann ein einfaches Programm in ein Codeobjekt kompiliert und zerlegt werden, um Opcodes zu erhalten, die von der virtuellen Python-Maschine ausgeführt werden. Dies ist in Listing 2.3 dargestellt.

1         >>> def square(x):
2         ...     return x*x
3         ... 
4 
5         >>> dis(square)
6         2           0 LOAD_FAST                0 (x)
7                     2 LOAD_FAST                0 (x)
8                     4 BINARY_MULTIPLY     
9                     6 RETURN_VALUE        

Codeauflistung 2.3: Zerlegen einer Funktion in Python Die

Header-Datei ./Include/opcodes.h enthält eine vollständige Liste aller Anweisungen / Opcodes für die virtuelle Python-Maschine. Opcodes sind ziemlich einfach. Nehmen Sie unser Beispiel in Listing 2.3, das vier Anweisungen enthält. LOAD_FAST lädt den Wert seines Arguments (in diesem Fall x) auf den Wertestapel. Die virtuelle Python-Maschine ist stapelbasiert, sodass die Werte für Opcode-Operationen aus dem Stapel "gepoppt" werden und die Berechnungsergebnisse zur weiteren Verwendung durch andere Opcodes auf den Stapel zurückgeschoben werden. Dann nimmt BINARY_MULTIPLY zwei Elemente vom Stapel, führt eine binäre Multiplikation beider Werte durch und schiebt das Ergebnis zurück auf den Stapel. RETURN VALUE- AnweisungRuft einen Wert vom Stapel ab, setzt den Rückgabewert für das Objekt auf diesen Wert und verlässt die Interpreterschleife. Wenn Sie sich Listing 2.3 ansehen, ist klar, dass dies eine ziemlich starke Vereinfachung ist.

Die aktuelle Erläuterung der Interpreterschleife berücksichtigt nicht eine Reihe von Details, die in den folgenden Kapiteln erläutert werden. Hier sind zum Beispiel die Fragen, auf die wir keine Antwort erhalten haben:

  • Woher stammen die von der LOAD_FAST-Anweisung geladenen Werte?
  • Woher kommen die Argumente, die als Teil der Anleitung verwendet werden?
  • Wie werden verschachtelte Funktions- und Methodenaufrufe behandelt?
  • Wie behandelt die Interpreterschleife Ausnahmen?

Nachdem Sie alle Anweisungen ausgeführt haben, setzt die Py_Main-Funktion die Ausführung fort, diesmal wird jedoch der Reinigungsprozess gestartet. Wenn Py_Initialize aufgerufen wird, um die Initialisierung während des Starts des Interpreters durchzuführen, wird Py_FinalizeEx aufgerufen, um die Bereinigung durchzuführen. Dieser Prozess umfasst das Warten auf das Beenden der Threads, das Aufrufen von Exit-Handlern sowie das Freigeben des vom Interpreter zugewiesenen noch verwendeten Speichers.

Daher haben wir uns die "allgemeine" Beschreibung der Prozesse angesehen, die in der ausführbaren Python-Datei auftreten, wenn ein Skript ausgeführt wird. Wie bereits erwähnt, müssen noch viele Fragen beantwortet werden. In Zukunft werden wir uns mit dem Studium des Dolmetschers befassen und jede der Stufen im Detail betrachten. Zunächst werden wir den Kompilierungsprozess im nächsten Kapitel beschreiben.

All Articles