Pythons Präprozessor
(pydong.org)- Python kann mithilfe der magischen Kommentare zur Quellcode-Kodierung in den ersten beiden Zeilen und benutzerdefinierter Codecs Dateiinhalte vor der Ausführung verändern oder durch völlig anderen Code ersetzen
- Benutzerdefinierte Codecs können während der Initialisierung des Interpreters über die Ausführung von
importin einer.pth-Pfadkonfigurationsdatei registriert werden; mitcodecs.registerwird eine Suchfunktion hinzugefügt - Eine Codec-Implementierung benötigt
decode(data: bytes) -> tuple[str, int]und einen inkrementellen Decoder; ohne Exception-Handling sieht man statt der eigentlichen Ursache unter Umständen nurSyntaxError: encoding problem: your_codec - Inkrement-/Dekrement-Operatoren
++/--, Python mit geschweiften Klammern, die Ausführung von C/C++ übercppyyund die Validierung von TOML mit JSON Schema lassen sich über denselben Einstiegspunkt implementieren - Über spielerische Beispiele hinaus kann dies auch für Python-Erweiterungen und Backports wie
pythonql,future-typing,future-fstringsundfuture-annotationsgenutzt werden;magic_codecreduziert wiederkehrende Arbeit
Die Quellcode-Kodierung als Einstiegspunkt für die Vorverarbeitung nutzen
- Gemäß PEP-0263 kann in einer der ersten beiden Zeilen einer Python-Datei die Quellcode-Kodierung angegeben werden
- Beispiel:
# coding=utf8,# -*- coding: utf8 -*-,# vim: set fileencoding=utf8 :
- Beispiel:
- Die magische Zeile muss zum regulären Ausdruck
^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)passen- Der Codec-Name muss zu
[-_.a-zA-Z0-9]+passen
- Der Codec-Name muss zu
- Ein benutzerdefinierter Codec muss die Quelle nicht nur dekodieren, sondern kann den Quellstring verändern und anschließend an den Python-Interpreter übergeben
Codecs mit .pth-Dateien registrieren
- Wenn der Python-Interpreter ohne die Option
-Sstartet, wird während der Initialisierung das Paketsitegeladen .pth-Pfadkonfigurationsdateien insite-packagesfügen Inhalte, ausgenommen leere Zeilen und Zeilen, die mit#beginnen, zum Modulsuchpfad hinzu- Laut Python-Dokumentation werden Zeilen ausgeführt, in denen auf
importein Leerzeichen oder Tab folgt- Beispiel: Wenn
packagename.pthimport packagename.register_codecenthält, wird dieses Modul während der Initialisierung importiert
- Beispiel: Wenn
- Das importierte Modul kann
codecs.registeraufrufen, um eine Codec-Suchfunktion zu registrieren- Der Import wird nur einmal ausgeführt, daher wird auch die Suchfunktion nur einmal registriert
Einen benutzerdefinierten Codec implementieren
- Ein benutzerdefinierter Codec benötigt zwei Dinge
decode(data: bytes) -> tuple[str, int]- Eine Klasse für einen inkrementellen Decoder
- Die Funktion
decodekann mitcodecs.utf_8_decodedie eigentliche UTF-8-Dekodierung durchführen und den Ergebnisstring an eine Vorverarbeitungsfunktion übergeben - Wenn Exceptions innerhalb des Codecs nicht abgefangen werden, wird statt eines normalen Tracebacks unter Umständen nur
SyntaxError: encoding problem: your_codecausgegeben- Exceptions aus der Vorverarbeitungsfunktion sollten daher mit
traceback.print_exc()direkt ausgegeben und anschließend erneut geworfen werden
- Exceptions aus der Vorverarbeitungsfunktion sollten daher mit
- Der inkrementelle Decoder kann die gesamte Datei in einem Buffer sammeln und die Vorverarbeitung nur einmal beim letzten
decode-Aufruf durchführen- Eine Beispielimplementierung erbt von
codecs.BufferedIncrementalDecoderund verarbeitet indecode(self, data, final=False)nur dann, wennfinalgesetzt ist
- Eine Beispielimplementierung erbt von
- Das Ergebnis der Vorverarbeitung muss nicht zwingend den Inhalt der Originaldatei verwenden; es kann auch vollständig beliebigen Python-Code zurückgeben
- Allerdings wird die erste Zeile als magische Zeile erwartet und daher entfernt, und das Ergebnis muss gültiges Python sein
Beispiele für Python-Syntaxerweiterungen
-
Inkrement- und Dekrement-Operatoren
++und--- Python hat keine unären Inkrement-/Dekrement-Operatoren
x++,x--sind syntaktisch ungültig++x,--xsind syntaktisch zwar gültig, werden aber jeweils zu Aufrufen vonx.__pos__().__pos__()bzw.x.__neg__().__neg__()- Ein Präprozessor kann den Token-Stream so verändern, dass er sich wie ein Inkrement-/Dekrement-Operator verhält
x++→(x, x := x + 1)[0]x--→(x, x := x - 1)[0]++x→(x, x := x + 1)[1]--x→(x, x := x - 1)[1]
- Diese Transformation nutzt den Python-Zuweisungsausdruck, den Walrus Operator
- Mit bloßer Token-Ersetzung kann dies bei Ausdrücken wie
x++ - -yfehlschlagen; mit Klammern wiex++ - (-y)lässt sich die Mehrdeutigkeit verringern - incdec.py ersetzt per regulärem Ausdruck, kann aber trotz Versuchen, Ersetzungen innerhalb von String-Literalen zu vermeiden, anfällig sein
- Eine Implementierung, die den Token-Stream direkt modifiziert, findet sich in magic.incdec
-
Python mit geschweiften Klammern
from __future__ import braceslöstSyntaxError: not a chanceaus- Ein Präprozessor kann den Token-Stream verändern und Scopes mit geschweiften Klammern in einrückungsbasiertes Python übersetzen
- Der Implementierungsablauf sieht folgendermaßen aus
- Tokens mit
tokenize.generate_tokenserzeugen - Den Eingabestring zeilenweise über
readlinevonio.StringIObereitstellen - Vorhandene
INDENT- undDEDENT-Tokens entfernen - Bei
{die Einrückungsebene erhöhen und:ausgeben - Bei
}die Einrückungsebene verringern - Nach
NLeinINDENT-Token entsprechend der aktuellen Einrückungsebene hinzufügen
- Tokens mit
- Um Konflikte mit Python-Dictionary-Literalen zu reduzieren, kann man die Einrückungsebene nur dann anpassen, wenn auf
{ein Zeilenumbruch folgt, und}nur dann als schließenden Scope behandeln, wenn davor ein Zeilenumbruch steht - Mehrzeilige Dictionaries können mit Backslashes genutzt werden, weil innerhalb der geschweiften Klammern dann kein Newline-Token entsteht
Andere Sprachen als Python ausführen
-
C und C++
- Bei Sprachen wie Shell-Skripten, CMake-Skripten, PHP oder Ruby, die
#für Kommentare verwenden, lässt sich eine magische Kodierungszeile leicht zusammen mit einem Shebang einfügen - In C und C++ verwenden Kommentare zwar
/* ... */oder// ..., aber Präprozessor-Direktiven beginnen mit#, sodass sie zum Encoding-Regex passen können - Eine beispielhafte magische Zeile ist auch in C/C++-Quellcode gültig und passt zugleich zu Pythons Encoding-Muster
#define CODEC "coding:magic.cpp"
- Mit cppyy kann Python C/C++-Code interpretieren und Bindings erzeugen
cppyyverwendet interncling
- Das Ergebnis der Vorverarbeitung wird ungefähr zu folgendem Python-Code
import cppyycppyy.cppdef("<input source file content>")from cppyy.gbl import main- Aufruf von
main()aus C/C++, wenn__name__ == "__main__"gilt
- Eine Beispielimplementierung findet sich in magic.cpp
- Bei Sprachen wie Shell-Skripten, CMake-Skripten, PHP oder Ruby, die
Als TOML-Validierungstool verwenden
- Da TOML Kommentare mit
#beginnt, kann eine magische Kodierungszeile wie# coding: magic.tomleingefügt werden - Wenn das Ergebnis der Vorverarbeitung durch ein Python-Validierungsskript ersetzt wird, kann der Python-Interpreter wie ein TOML-Validierungstool verwendet werden
- Das Validierungsbeispiel nutzt folgende Module
tomllib, um die TOML-Datei zu lesenjson, um die JSON-Schema-Datei zu lesen- jsonschema zur Validierung
- Ausführungsbeispiel:
python tests/toml/data_valid.toml -s tests/toml/schema.json- Wenn die Datei gültig ist, wird
Successfully validated.ausgegeben
- Im Beispiel mit ungültigem TOML wird ein Validierungsfehler ausgegeben, weil der String
'20'im Arrayscoreskeine Zahl ist - Eine Beispielimplementierung findet sich in magic.toml
Praktische Nutzung und magic_codec
- Mit benutzerdefinierten Codecs und
.pth-Dateien zusammen lässt sich das Verhalten des Python-Interpreters stark verändern - Die meisten Beispiele sind zum Spaß gedacht, es gibt aber auch praktische Anwendungsfälle
- pythonql: Query-Language-Erweiterung für Python
- future-typing: Backport von generischen Type Hints und
|-Union-Syntax nach Python 3.6+ - future-fstrings
- future-annotations
- Wer
site-packagesnicht direkt anfassen oder.pth-Dateien und wiederkehrenden Code nicht selbst schreiben möchte, kann magic_codec verwenden magic_codec-Erweiterungen können als Python-Pakete mit dem Präfixmagic_erstellt werden- Wenn der Datei-Codec auf
magic_foogesetzt ist, wird das Paketmagic_foogeladen - Es wird geprüft, ob dieses Paket eine Funktion
preprocessenthält
- Wenn der Datei-Codec auf
- Die erwartete
preprocess-Signatur lautet wie folgtdef preprocess(data: str) -> str:
- Erweiterungsbeispiele finden sich unter example/
1 Kommentare
Hacker-News-Meinungen
Führt man
from __future__ import bracesaus, erscheint die witzige FehlermeldungSyntaxError: not a chance; diese Formulierung war seit 2001 in CPython fest eincodierthttps://github.com/python/cpython/commit/ad3d3f2f3f19833f59f...
Der Autor Jeremy Hylton arbeitet heute bei Google als Principal Engineer für die Qualität der KI-Suche. Ziemlich beeindruckend, dass seine Karriere in 24 Jahren von einer scherzhaften Würdigung verbotener Syntax zu einem universellen Abfragesystem geführt hat, das keine eigene Syntax braucht
break rust;im Rust-Compiler einen internen Compilerfehler auslöste. Ich frage mich, wie viele ähnliche Easter Eggs es in anderen Sprachen gibtIch halte es für ein Missverständnis, spielerisches, informelles Hobby-Hacking als eine von professioneller Entwicklung getrennte Welt zu betrachten
https://news.ycombinator.com/item?id=41314393
Ich dachte, mit Import-Hooks herumzuspielen sei die kreativste Art, gefeuert zu werden, aber das war naiv. Schade, dass man wegen der Codec-Regex wohl nicht richtig mit Dingen wie
μtf8trollen kann; jetzt bleibt wohl nur noch, mit Import-Hooks, einem Präprozessor undsys.settracealle Funktionen auf die unmittelbar zuvor aufgerufene Funktion zu monkeypatchen und alle 17 Minuten stdout und stderr zu vertauschenEs gibt gute Gründe dafür, dass Python Präprozessor-Hooks nicht absichtlich sichtbar macht, und vernünftige Erwachsene sollten sich davon meiner Meinung nach fernhalten
Andererseits möchte ich mit vernünftigen Erwachsenen nichts zu tun haben. Damit ließe sich vermutlich sehr viel wirklich Lustiges anstellen
public/private-Sichtbarkeitsmodifikatoren, und vermutlich hat man auch allerlei Metaprogramming-Magie offengelassen. Wenn man sich um „vernünftige Erwachsene“ gesorgt hätte, wären solche Designentscheidungen etwas seltsam gewesen :)Das wirkt praktisch und wirklich nützlich. Wenn ich unsinnige Import-Hacks gebaut habe, habe ich normalerweise ein Modul importiert, den Code mit dem
ast-Modul umgeschrieben, ihn perexecausgeführt und dann einexit()eingeschoben; mit einem Präprozessor wäre das viel angenehmer zu benutzenBevor alle
dicts die Reihenfolge garantiert haben, habe ich AST-Rewriting hauptsächlich genutzt, um Listenliterale in Ordered-Dict-Aufrufe umzuwandeln, und das war tatsächlich nützlichIch mag die Flexibilität von Python. Das Verfluchteste, was ich je getan habe, war, Strings in-place zu verändern, und am Ende habe ich sogar
mmapmissbraucht, um ein Skript zu schreiben, das sich selbst verändert. Jetzt habe ich das Gefühl, als Producer einen Lisp-Interpreter schreiben zu müssenctypesdirekt an Speicheradressen geschrieben?Der beste Anwendungsfall, den ich bisher gefunden habe, ist das von JSX inspirierte pyxl: https://github.com/dropbox/pyxl
Damit kann man solchen Code schreiben
# coding: pyxlprint Hello World!Ich frage mich, ob man damit den Übergang von Python 2 zu 3 besser hätte bewältigen können. Zum Beispiel so, dass
# coding: six.python2Python-2-Code in gültigen Python-3-Code überführt oder# coding: six.python3Python-3-Code so umwandelt, dass er unter Python 2 läuftAuch das Hinzufügen oder Entfernen von Präfixen wie
b"..."oderu"..."wäre wohl möglich gewesendict-Keys verwendet, zeigten sie daher auf denselben Eintrag. In Python 3 zeigenbytesundstrmit demselben ASCII-Inhalt innerhalb desselbendictauf unterschiedliche EinträgeEs gibt noch kniffligere Änderungen. Viele eingebaute Funktionen wie
.keys()und.values()geben in Python 2 Listen zurück, in Python 3 dagegen Iteratoren. Wenn man Code mitsix-Utilities oder anderen Workarounds sicher übersetzen will, wird er sehr ausschweifend, weil die meisten Werte nur einmal, manche aber gelegentlich zweimal verwendet werdenWenn es ein Tool gibt, das Code beim Import umschreiben kann, wäre es meiner Meinung nach besser, den transformierten Code zu committen und schrittweise aufzuräumen. Der schwierige Teil sind Verhaltensänderungen wie
strgegenüberbytes, die sich auf weit entfernte Codebereiche auswirkenWerden Abhängigkeiten, die durch diese Coding-Hook-Strategie entstehen, von
pip freezeoderuverkannt?Wenn nicht, dürfte das lustig werden :). Wenn jemand so etwas eingebaut hat, ist fast garantiert, dass es noch weitere Fallstricke gibt; statt gegen solche Drachen zu kämpfen, wäre es wahrscheinlich einfacher, die Bibliothek neu zu schreiben
Es wäre ziemlich witzig, Pseudocode-Python zu bauen und es von einem LLM decodieren zu lassen. Natürlich wäre das furchtbar, aber Spaß machen würde es wohl