Zum Inhalt springen

cat posts/python-skript-unter-der-haube.md

Was passiert eigentlich bei `python skript.py`?

Vom Quelltext zum laufenden Programm: Tokenizer, AST, Code-Objekte, Bytecode und die CPython-Ausführungsengine – plus ein Blick auf Cache, Spezialisierung, Free-Threading und JIT.

Was passiert eigentlich bei `python skript.py`?

In Teil 1 der Python-Serie hieß es schlicht:

python skript.py

Python liest die Datei ein und führt sie aus. So weit, so unspektakulär.

Aber was passiert zwischen dem Drücken der Eingabetaste und der ersten Ausgabe? Python wird gerne als interpretierte Sprache bezeichnet. Für CPython greift das etwas kurz: Der Quelltext wird zunächst analysiert und zu Bytecode kompiliert. Erst danach beginnt die eigentliche Ausführung.

Schauen wir uns an, was dabei unter der Haube passiert.

Python ist nicht gleich CPython

Zunächst müssen wir zwei Begriffe auseinanderhalten:

  • Python ist die Programmiersprache.
  • CPython ist die am weitesten verbreitete Implementierung dieser Sprache.

CPython ist größtenteils in C geschrieben und die Implementierung, die Du normalerweise über python.org oder den Paketmanager Deiner Linux-Distribution installierst.

Daneben gibt es andere Implementierungen wie PyPy oder GraalPy. Sie verstehen weitgehend dieselbe Sprache, können Python-Code intern aber ganz anders ausführen.

Wenn wir uns hier Bytecode, den GIL oder den experimentellen JIT ansehen, reden wir deshalb ausdrücklich über CPython.

Vom Quelltext zur Ausführung

Bei

python skript.py

läuft vereinfacht diese Pipeline ab:

  1. Tokenizing: Der Quelltext wird in Tokens zerlegt.
  2. Parsing: Aus diesen Tokens entsteht ein Abstract Syntax Tree.
  3. Compiling: Der AST wird in ein Code-Objekt mit Bytecode übersetzt.
  4. Execution: Die CPython-Ausführungsengine verarbeitet diesen Bytecode.

Dazu kommen einige Dinge drumherum: CPython muss zunächst den Interpreter initialisieren, sys.path aufbauen, das Top-Level-Modul __main__ vorbereiten und später eventuell weitere Module importieren.

Bei einem kleinen Skript passiert all das so schnell, dass Du davon normalerweise nichts bemerkst.

Schritt 1: Tokenizing

Nehmen wir eine einfache Python-Zeile:

ergebnis = 2 + 3

Für uns besteht sie aus einem Variablennamen, einer Zuweisung und einer Addition. Der Tokenizer zerlegt den Text zunächst in einzelne Bestandteile.

Vereinfacht sieht das so aus:

NAME        ergebnis
OP          =
NUMBER      2
OP          +
NUMBER      3
NEWLINE

Solche Bestandteile heißen Tokens. Dazu gehören unter anderem:

  • Namen wie ergebnis
  • Zahlen und Strings
  • Keywords wie if, while oder def
  • Operatoren wie +, - oder ==
  • Einrückungen und Zeilenenden

Der Tokenizer entscheidet noch nicht, was das gesamte Programm bedeutet. Er macht aus dem Zeichenstrom zunächst Einheiten, mit denen der Parser weiterarbeiten kann.

Du kannst Dir die Tokens einer Datei mit dem Standardmodul tokenize ansehen:

python -m tokenize skript.py

Die echte Ausgabe enthält zusätzlich Positionen und weitere Informationen. Für den normalen Programmieralltag braucht man das selten, es zeigt aber schön, dass CPython den Quelltext nicht einfach Zeichen für Zeichen ausführt.

Schritt 2: Parsing und AST

Der Parser prüft, wie die Tokens gemäß der Python-Grammatik zusammengehören.

Aus

ergebnis = 2 + 3

entsteht dabei eine Baumstruktur, die sinngemäß so aussieht:

Zuweisung
├── Ziel: ergebnis
└── Wert: Addition
    ├── 2
    └── 3

Diese Struktur heißt Abstract Syntax Tree, kurz AST.

Der AST beschreibt die Struktur und Bedeutung des Codes, enthält aber nicht mehr jedes Detail seiner Schreibweise. Ob Du

2+3

oder

2 + 3

schreibst, ergibt denselben grundlegenden Syntaxbaum.

Mit dem Standardmodul ast kannst Du Dir diesen Baum selbst ansehen:

import ast

code = "ergebnis = 2 + 3"
baum = ast.parse(code)

print(ast.dump(baum, indent=4))

Die Ausgabe sieht gekürzt etwa so aus:

Module(
    body=[
        Assign(
            targets=[
                Name(id='ergebnis')
            ],
            value=BinOp(
                left=Constant(value=2),
                op=Add(),
                right=Constant(value=3)
            )
        )
    ]
)

Hier lässt sich ziemlich direkt ablesen: Wir haben eine Zuweisung an ergebnis, deren rechte Seite aus einer Addition zweier Konstanten besteht.

Viele Syntaxfehler werden bereits beim Tokenizing oder Parsing erkannt. Fehlt zum Beispiel der Doppelpunkt hinter einer if-Bedingung:

if hp > 0
    print("Du lebst.")

kann daraus kein gültiger Python-Code entstehen. Das Programm endet mit einem SyntaxError, bevor seine eigentlichen Anweisungen ausgeführt werden.

Schritt 3: Compiling zu einem Code-Objekt

Aus dem AST erzeugt der CPython-Compiler ein Code-Objekt.

Ein solches Objekt enthält unter anderem:

  • Bytecode
  • Konstanten
  • verwendete Namen
  • Informationen über lokale Variablen
  • Positionen im Quelltext
  • weitere Metadaten, die CPython für die Ausführung benötigt

Mit der eingebauten Funktion compile(...) kannst Du diesen Schritt selbst ausführen:

code = compile(
    "ergebnis = 2 + 3",
    filename="<beispiel>",
    mode="exec",
)

print(code)

Die Ausgabe ähnelt:

<code object <module> at 0x..., file "<beispiel>", line 1>

Der Quelltext wurde jetzt kompiliert, aber noch nicht ausgeführt.

Das kannst Du anschließend mit exec(...) erledigen:

exec(code)

Bei einem normalen Skript übernimmt CPython diese Schritte automatisch.

Bytecode: Instruktionen für CPython

Im Code-Objekt steckt der Bytecode. Das sind Instruktionen für die CPython-Ausführungsengine.

Bytecode ist kein Maschinencode für Deine CPU. Ein x86- oder ARM-Prozessor kann Instruktionen wie LOAD_FAST oder BINARY_OP nicht direkt ausführen. Im normalen CPython-Betrieb werden sie von der Ausführungsengine verarbeitet.

Nehmen wir eine Funktion:

def addiere(a, b):
    return a + b

Mit dem Standardmodul dis kannst Du ihren Bytecode disassemblieren:

import dis


def addiere(a, b):
    return a + b


dis.dis(addiere)

Je nach CPython-Version sieht die Ausgabe ungefähr so aus:

RESUME
LOAD_FAST       a
LOAD_FAST       b
BINARY_OP       +
RETURN_VALUE

Vereinfacht bedeuten diese Instruktionen:

  • RESUME markiert den Einstieg in den ausführbaren Code.
  • LOAD_FAST lädt die lokalen Variablen a und b.
  • BINARY_OP führt die Addition aus.
  • RETURN_VALUE liefert das Ergebnis zurück.

Die konkreten Opcodes und ihre Bedeutung können sich zwischen Python-Versionen ändern. CPython garantiert ausdrücklich kein stabiles Bytecode-Format über verschiedene Releases hinweg.

Bytecode ist deshalb ein spannendes Werkzeug zum Verstehen von CPython, aber keine Schnittstelle, auf deren konkrete Opcodes normaler Anwendungscode angewiesen sein sollte.

Eine komplette Datei kannst Du ebenfalls disassemblieren:

python -m dis skript.py

Schritt 4: Den Bytecode ausführen

Sobald ein Code-Objekt vorliegt, beginnt die eigentliche Ausführung.

CPython verarbeitet die Bytecode-Instruktionen und führt die jeweils zugehörigen Operationen aus. Dabei spielen unter anderem folgende Strukturen eine Rolle:

  • ein Evaluation Stack für Zwischenwerte
  • lokale und globale Namespaces
  • Python-Objekte wie int, str oder list
  • Frames für Funktionsaufrufe
  • Exception Handling
  • Referenzzählung und Garbage Collection

Bei einem Ausdruck wie

2 + 3

werden beispielsweise die beteiligten Werte bereitgestellt, die passende Additionsoperation ausgeführt und das Ergebnis wieder als Python-Objekt behandelt.

Für diese Ausführungsumgebung wird gelegentlich der Begriff virtuelle Maschine verwendet. Gemeint ist damit keine vollständige virtuelle Maschine wie VirtualBox oder VMware, die einen ganzen Computer nachbildet. CPython stellt eine abstrakte Laufzeitumgebung für seinen eigenen Bytecode bereit.

Das Skript läuft als __main__

Beim Start des Interpreters existiert bereits ein spezielles Top-Level-Modul:

__main__

Der Code aus dem direkt gestarteten Skript wird in diesem Top-Level-Kontext ausgeführt.

Deshalb ergibt eine Datei mit

print(__name__)

bei

python skript.py

die Ausgabe:

__main__

Importierst Du dieselbe Datei dagegen als reguläres Modul, enthält __name__ ihren Modulnamen.

Darauf beruht das bekannte Muster:

def main():
    print("Das Programm startet.")


if __name__ == "__main__":
    main()

main() wird damit nur aufgerufen, wenn die Datei als Top-Level-Programm ausgeführt wird. Beim normalen Import stehen ihre Funktionen und Klassen zur Verfügung, ohne dass dieser Block ausgeführt wird.

Das gilt übrigens nicht nur für python skript.py. Auch python -m modul, python -c "...", Code von stdin und die interaktive REPL laufen in einem Top-Level-Kontext mit dem Namen __main__.

Auf Module und verschiedene Startvarianten gehen wir im späteren Artikel über Module und Projektstruktur genauer ein.

Was passiert mit dem Importpfad?

CPython baut beim Start den Modul-Suchpfad auf, den Du über sys.path sehen kannst:

import sys

print(sys.path)

Bei einem normalen Aufruf wie

python skript.py

steht standardmäßig das Verzeichnis des gestarteten Skripts am Anfang dieses Suchpfads.

Führt Dein Skript anschließend

import monster

aus, gehört dieses Verzeichnis deshalb zu den ersten Orten, an denen Python nach monster sucht.

Das erklärt eine bekannte Stolperfalle. Legst Du neben Dein Skript beispielsweise eine Datei namens

json.py

kann sie das gleichnamige Modul aus der Standard Library überschatten.

Ein

import json

lädt dann möglicherweise Deine lokale Datei statt des erwarteten Standardmoduls.

Das Verhalten lässt sich mit Optionen wie -P beziehungsweise PYTHONSAFEPATH beeinflussen, für den normalen Start eines Skripts ist das Skriptverzeichnis aber Teil des Suchpfads.

__pycache__: kompilierter Bytecode auf der Festplatte

Beim Import eines Python-Moduls speichert CPython den kompilierten Bytecode normalerweise als .pyc-Datei in einem __pycache__-Verzeichnis.

Ein Projekt könnte beispielsweise so aussehen:

projekt/
├── dungeon.py
├── monster.py
└── __pycache__/
    └── monster.cpython-314.pyc

cpython-314 kennzeichnet hier eine Cache-Datei für CPython 3.14.

Bei einem späteren Import prüft Python, ob die Cache-Datei noch zur Quelle passt. Je nach verwendeter Cache-Variante geschieht das beispielsweise anhand von Zeitstempeln oder Hashes.

Ist die .pyc noch gültig, muss CPython den Quelltext nicht erneut parsen und zu Bytecode kompilieren. Das Code-Objekt kann aus dem Cache geladen werden.

Das kann Imports beschleunigen.

Was eine .pyc nicht macht: Deinen laufenden Python-Code schneller.

Nach dem Laden wird derselbe Bytecode ausgeführt. Eine gecachte Funktion, Schleife oder Berechnung läuft nicht deshalb schneller, weil sie aus einer .pyc stammt.

Das direkt gestartete Skript wird normalerweise nicht gecacht

Ein wichtiges Detail:

python dungeon.py

erzeugt normalerweise keine .pyc-Datei für dungeon.py.

Der Grund ist einfach: Das Top-Level-Skript wird ausgeführt, aber nicht über den normalen Importmechanismus importiert.

Enthält dungeon.py dagegen

import monster

kann für monster.py sehr wohl eine Cache-Datei entstehen:

__pycache__/monster.cpython-314.pyc

Gehört __pycache__ ins Git?

Normalerweise nicht. Die Dateien werden automatisch erzeugt und hängen von der Python-Implementierung sowie deren Version ab.

Typische Einträge in .gitignore sind:

__pycache__/
*.py[cod]

Einen __pycache__-Ordner kannst Du normalerweise problemlos löschen. Python legt die benötigten Dateien bei späteren Imports erneut an.

Mit

python -B skript.py

verhinderst Du das Schreiben von .pyc-Dateien.

Dasselbe geht über eine Umgebungsvariable:

PYTHONDONTWRITEBYTECODE=1 python skript.py

Die interne Kompilierung verschwindet dadurch nicht. CPython erzeugt weiterhin Code-Objekte und Bytecode, speichert den Cache nur nicht auf der Festplatte.

Der spezialisierende Interpreter

Die bisherige Beschreibung

Bytecode
   ↓
Interpreter
   ↓
Python-Operation

ist korrekt, aber inzwischen etwas vereinfacht.

Seit Python 3.11 besitzt CPython einen specializing adaptive interpreter. Die Ausführungsengine kann bestimmte Operationen während der Laufzeit an die tatsächlich auftretenden Datentypen anpassen.

Nehmen wir wieder:

def addiere(a, b):
    return a + b

a + b kann in Python sehr unterschiedliche Dinge bedeuten:

1 + 2

addiert zwei Ganzzahlen,

1.5 + 2.5

zwei Fließkommazahlen und

"Dun" + "geon"

zwei Strings.

Auch eigene Klassen können definieren, wie sich + für ihre Objekte verhalten soll.

Eine allgemeine BINARY_OP-Operation muss deshalb mit vielen Fällen umgehen können.

Wird eine bestimmte Stelle im Programm immer wieder mit denselben passenden Typen ausgeführt, kann CPython sie spezialisieren. Aus einer allgemeinen Operation wird intern eine Variante, die besser auf den beobachteten Fall zugeschnitten ist.

Der Python-Code selbst ändert sich dadurch nicht:

a + b

bleibt a + b. Die Optimierung findet unter der Oberfläche statt.

Spezialisierung mit dis sichtbar machen

dis kann neben dem ursprünglichen Bytecode auch den während der Laufzeit angepassten Bytecode und seine internen Caches anzeigen:

import dis


def addiere(a, b):
    return a + b


for _ in range(100_000):
    addiere(1, 2)

dis.dis(addiere, adaptive=True, show_caches=True)

Welche spezialisierten Instruktionen Du dort siehst, hängt von der CPython-Version, dem Build und dem tatsächlich ausgeführten Code ab.

In Python 3.14 gibt es für das Kommandozeilenwerkzeug außerdem die Option -S, um spezialisierten Bytecode anzuzeigen:

python -m dis -S skript.py

Genau wegen dieser Anpassungen kann der Bytecode einer bereits häufig ausgeführten Funktion intern anders aussehen als direkt nach dem Kompilieren.

Free-Threading: CPython ohne dauerhaft aktiven GIL

Der reguläre CPython-Build arbeitet weiterhin mit dem Global Interpreter Lock, kurz GIL.

Vereinfacht sorgt der GIL im normalen Build dafür, dass innerhalb eines Interpreters nur ein Thread gleichzeitig Python-Code ausführt.

Threads sind dadurch keineswegs nutzlos. Bei I/O-lastigen Aufgaben wie Netzwerkzugriffen, Dateizugriffen oder dem Warten auf externe Systeme können sie sehr sinnvoll sein. CPU-intensive Python-Berechnungen lassen sich mit mehreren Threads im normalen CPython-Build aber nicht einfach parallel über mehrere CPU-Kerne verteilen.

Seit Python 3.13 gibt es zusätzlich einen Free-Threaded Build von CPython, der ohne GIL laufen kann.

Mit Python 3.14 hat dieser Modus die nächste Stufe erreicht: Free-Threaded CPython gilt nun als offiziell unterstützt, bleibt aber eine optionale Build-Variante. Der normale CPython-Build wurde also nicht plötzlich GIL-frei.

Unter Windows und macOS bieten die offiziellen Installer seit Python 3.13 eine Free-Threaded Variante optional an. Auf anderen Plattformen hängt die Verfügbarkeit davon ab, wie Python gebaut oder paketiert wurde.

Ein Free-Threaded Build ermöglicht tatsächlich parallele Ausführung von Python-Code in mehreren Threads auf verschiedenen CPU-Kernen.

Das bedeutet allerdings nicht:

GIL aus = jedes Python-Programm automatisch schneller

Der Code muss überhaupt Arbeit besitzen, die sich sinnvoll parallelisieren lässt. Gemeinsam verwendete Daten benötigen weiterhin korrekte Synchronisation, und Free-Threading verursacht selbst zusätzlichen Verwaltungsaufwand.

Ein weiterer Punkt sind C-Extensions. Erweiterungen müssen den Free-Threaded Betrieb unterstützen. Importierst Du eine Extension, die nicht entsprechend gekennzeichnet ist, kann CPython den GIL für den Prozess wieder aktivieren und eine Warnung ausgeben.

Ob der GIL im aktuell laufenden Prozess aktiv ist, kannst Du prüfen:

import sys

print(sys._is_gil_enabled())

Bei einem Free-Threaded Build lässt sich der GIL bei Bedarf sogar wieder einschalten, beispielsweise mit:

python -X gil=1 skript.py

Free-Threading ist damit inzwischen eine unterstützte Alternative, aber noch nicht der Standardbetrieb von CPython.

Der experimentelle JIT

Noch weiter unter die Haube führt der Just-in-Time-Compiler, kurz JIT.

Die vereinfachte Idee eines JIT lautet: Häufig ausgeführter Code wird während der Laufzeit in nativen Maschinencode übersetzt. Dieser kann anschließend direkt von der CPU ausgeführt werden.

Bei CPython steckt zwischen Bytecode und Maschinencode allerdings noch eine weitere Stufe.

Seit Python 3.13 besitzt CPython intern eine sogenannte Tier-2-IR. Sie wird häufig auch als Micro-Ops oder uops bezeichnet.

Der Ablauf sieht stark vereinfacht so aus:

Python-Quelltext
        ↓
      AST
        ↓
Tier-1-Bytecode
        ↓
  Spezialisierung
        ↓
heiße Code-Pfade
        ↓
Tier-2-Micro-Ops
        ↓
  Optimierungen
        ↓
       JIT
        ↓
nativer Maschinencode

Der spezialisierende Interpreter liefert dabei Informationen darüber, welche Pfade besonders häufig ausgeführt werden. Solche heißen Pfade können in die feingranularere Tier-2-Darstellung übersetzt und dort weiter optimiert werden.

Der JIT kompiliert anschließend diese optimierten Micro-Ops zu Maschinencode.

CPython verwendet dafür ein Verfahren namens copy-and-patch.

JIT in Python 3.14

Der JIT wurde in Python 3.13 zunächst als experimentelle Build-Option eingeführt.

Seit Python 3.14 enthalten die offiziellen Windows- und macOS-Binaries die experimentelle JIT-Unterstützung bereits. Sie ist allerdings standardmäßig deaktiviert.

In einem entsprechend gebauten CPython kannst Du sie beim Start aktivieren:

PYTHON_JIT=1 python skript.py

In der PowerShell:

$env:PYTHON_JIT = "1"
python skript.py

Ob Dein Python-Binary den JIT überhaupt enthält, kannst Du seit Python 3.14 über sys._jit prüfen:

import sys

print(sys._jit.is_available())
print(sys._jit.is_enabled())

is_available() sagt Dir, ob das gestartete Python überhaupt mit JIT-Unterstützung gebaut wurde.

is_enabled() zeigt, ob der JIT im aktuellen Prozess aktiviert ist.

Der JIT ist in Python 3.14 weiterhin ausdrücklich experimentell. Er ist nicht für Production empfohlen und ein aktivierter JIT bedeutet auch nicht automatisch mehr Performance. Je nach Workload kann er schneller, ähnlich schnell oder sogar langsamer als der normale Interpreter sein.

Free-Threaded Builds und der JIT lassen sich in Python 3.14 außerdem nicht kombinieren.

Für normalen Anwendungscode ist der JIT deshalb momentan eher etwas zum Experimentieren und ein Blick darauf, wohin sich CPythons Ausführungsengine entwickelt.

Wird Python nun kompiliert oder interpretiert?

Die Frage

Ist Python kompiliert oder interpretiert?

versucht zwei Begriffe auf eine Sprache zu kleben, obwohl die konkrete Implementierung viel interessanter ist.

Für klassisches CPython ist diese Beschreibung wesentlich genauer:

CPython kompiliert Python-Quelltext zunächst zu Bytecode und führt diesen anschließend mit seiner Ausführungsengine aus.

Bei C sieht der typische Ablauf stark vereinfacht so aus:

C-Quelltext
    ↓
Compiler
    ↓
nativer Maschinencode
    ↓
CPU

Beim normalen CPython-Betrieb:

Python-Quelltext
    ↓
Parser und Compiler
    ↓
Bytecode
    ↓
CPython-Ausführungsengine
    ↓
Operationen auf Python-Objekten

Und mit aktiviertem JIT kann für geeignete, häufig ausgeführte Pfade eine weitere Pipeline hinzukommen:

Python-Quelltext
    ↓
Bytecode
    ↓
spezialisierter Tier-1-Code
    ↓
Tier-2-Micro-Ops
    ↓
JIT
    ↓
nativer Maschinencode
    ↓
CPU

Die Aussage „Python ist interpretiert“ ist für eine erste Erklärung also nicht komplett daneben. Sie lässt nur einen ziemlich großen Teil dessen weg, was CPython tatsächlich macht.

Nützliche Kommandozeilenoptionen

Einige Teile dieses Ablaufs kannst Du mit Bordmitteln selbst untersuchen.

Code direkt ausführen

Mit -c übergibst Du Python-Code direkt auf der Kommandozeile:

python -c "print(2 ** 10)"

Ausgabe:

1024

Auch dieser Code wird kompiliert und anschließend ausgeführt. Es wird lediglich keine .py-Datei eingelesen.

Ein Modul als Programm starten

Mit -m lässt Du Python ein Modul oder Package über seinen Modulnamen starten:

python -m json.tool datei.json

Ein häufiges Beispiel ist das Anlegen einer Virtual Environment:

python -m venv .venv

Auch installierte Tools werden oft so gestartet:

python -m pytest

Das ist nicht exakt dasselbe wie

python irgendeine_datei.py

Bei -m sucht Python das angegebene Modul über das Importsystem und führt es anschließend als Top-Level-Code aus. Gerade innerhalb von Packages ist das wichtig, weil der Package-Kontext erhalten bleibt:

python -m dungeon.main

Relative Imports innerhalb des Packages können dadurch korrekt funktionieren.

Tokens anzeigen

Den Tokenizer kannst Du direkt auf eine Datei loslassen:

python -m tokenize skript.py

AST anzeigen

Für den AST gibt es keine ganz so komfortable Standard-Kommandozeile wie bei dis, aber mit ast.dump(...) reichen wenige Zeilen Python:

import ast
from pathlib import Path

code = Path("skript.py").read_text()
baum = ast.parse(code)

print(ast.dump(baum, indent=4))

Bytecode anzeigen

Eine Datei lässt sich direkt disassemblieren:

python -m dis skript.py

Unter Python 3.14 kannst Du mit -S zusätzlich spezialisierten Bytecode anzeigen:

python -m dis -S skript.py

Importzeiten messen

Wenn ein Programm bereits beim Start auffällig lange braucht, kann -X importtime helfen:

python -X importtime skript.py

CPython zeigt damit an, wie viel Zeit einzelne Imports benötigen.

Bei einem Projekt mit vielen Abhängigkeiten lässt sich so schnell erkennen, ob ein bestimmter Import einen überraschend großen Teil der Startzeit ausmacht.

Module vorkompilieren

Mit compileall kannst Du die importierbaren Python-Dateien eines Verzeichnisses vorkompilieren:

python -m compileall .

Dadurch entstehen die entsprechenden .pyc-Dateien.

Das kann beispielsweise bei bestimmten Deployments sinnvoll sein. Bei der normalen lokalen Entwicklung musst Du Dich darum meistens nicht kümmern.

Keine Bytecode-Dateien schreiben

Mit -B verhinderst Du das Speichern von .pyc-Dateien:

python -B skript.py

Der Code wird trotzdem intern zu einem Code-Objekt kompiliert. Lediglich der Cache auf der Festplatte entfällt.

Der gesamte Ablauf im Überblick

Startest Du

python skript.py

passiert vereinfacht Folgendes:

  1. Das Betriebssystem startet den Python-Prozess.
  2. CPython initialisiert den Interpreter und seine Laufzeitumgebung.
  3. Der Modul-Suchpfad sys.path wird aufgebaut.
  4. Der Top-Level-Kontext __main__ steht für das Programm bereit.
  5. CPython liest den Quelltext des Skripts.
  6. Der Tokenizer zerlegt ihn in Tokens.
  7. Der Parser erzeugt daraus einen AST.
  8. Der Compiler übersetzt den AST in Code-Objekte mit Bytecode.
  9. Die CPython-Ausführungsengine beginnt mit der Ausführung des Top-Level-Codes.
  10. Imports können weitere Module laden und deren Bytecode in __pycache__ speichern.
  11. Häufig ausgeführte Operationen können während der Laufzeit spezialisiert werden.
  12. Ein Free-Threaded Build kann Python-Code ohne dauerhaft aktiven GIL parallel in mehreren Threads ausführen.
  13. In einem unterstützten Build kann ein experimenteller JIT heiße Code-Pfade über Tier-2-Micro-Ops bis zu nativem Maschinencode optimieren.
  14. Wenn der Top-Level-Code beendet ist und keine weitere Arbeit ansteht, endet der Python-Prozess.

Für

print("Hallo")

ist das erstaunlich viel Technik.

Im Alltag musst Du davon kaum etwas wissen. Sobald Du aber verstehen willst, warum Imports manchmal merkwürdig reagieren, wo __pycache__ herkommt, was dis eigentlich zeigt oder weshalb neue Python-Versionen ohne Änderungen an Deinem Code schneller werden können, landet man ziemlich schnell genau hier.

Weiterlesen

0 Kommentare

Noch keine Kommentare. Sei der/die Erste!