PEP 437 – Eine DSL zur Spezifikation von Signaturen, Annotationen und Argumentkonvertern
- Autor:
- Stefan Krah <skrah at bytereef.org>
- Status:
- Abgelehnt
- Typ:
- Standards Track
- Erstellt:
- 11-Mrz-2013
- Python-Version:
- 3.4
- Post-History:
- Resolution:
- Python-Dev Nachricht
Zusammenfassung
Die Python C-API verfügt derzeit über keinen Mechanismus zur Spezifikation und automatischen Generierung von Funktionssignaturen, Annotationen oder benutzerdefinierten Argumentkonvertern.
Es gibt mehrere mögliche Ansätze für das Problem. Cython verwendet cdef-Definitionen in .pyx-Dateien zur Generierung der erforderlichen Informationen. Die C-API-Funktionen von CPython erfordern jedoch oft zusätzliche Initialisierungs- und Aufräumschnipsel, die in einem cdef schwer zu spezifizieren wären.
PEP 436 schlägt eine domänenspezifische Sprache (DSL) vor, die in C-Kommentaren eingeschlossen ist und weitgehend einer Konfigurationsdatei pro Parameter ähnelt. Ein Präprozessor liest den Kommentar und gibt eine Argument-Parsing-Funktion, Docstrings und einen Header für die Funktion aus, die die Ergebnisse des Parsing-Schritts verwendet.
Letztere Funktion wird im Folgenden als Implementierungsfunktion bezeichnet.
Ablehnungsbescheid
Diese PEP wurde von Guido van Rossum auf der PyCon US 2013 abgelehnt. Mehrere der spezifischen Probleme, die von dieser PEP aufgeworfen wurden, wurden jedoch bei der Gestaltung der zweiten Iteration der PEP 436 DSL berücksichtigt.
Begründung
Die Meinungen gehen auseinander, was die Eignung der PEP 436 DSL im Kontext einer C-Datei betrifft. Diese PEP schlägt eine alternative DSL vor. Die spezifischen Probleme mit PEP 436, die den Gegenentwurf ausgelöst haben, werden im letzten Abschnitt dieser PEP erläutert.
Umfang
Die PEP konzentriert sich ausschließlich auf die DSL. Themen wie die Ausgabeorte von Docstrings oder der generierte Code liegen außerhalb des Geltungsbereichs dieser PEP.
Es ist jedoch unerlässlich, dass die DSL zur Generierung benutzerdefinierter Argument-Parser geeignet ist, einer Funktion, die bereits in Cython implementiert ist. Daher ist eines der Ziele dieser PEP, die DSL nahe an bestehenden Lösungen zu halten und so eine mögliche Aufnahme der relevanten Teile von Cython in den CPython-Quellbaum zu erleichtern.
DSL-Übersicht
Typsicherheit und Annotationen
Eine Konvertierung von einem Python- zu einem C-Wert wird vollständig durch den Typ der Konverterfunktion definiert. Die PyArg_Parse*-Familie von Funktionen akzeptiert benutzerdefinierte Konverter zusätzlich zu den bekannten Standardkonvertern „i“, „f“ usw.
Diese PEP betrachtet die Standardkonverter als abstrakte Funktionen, unabhängig davon, wie sie tatsächlich implementiert sind.
Include/converters.h
Konverterfunktionen müssen vorwärts deklariert werden. Alle Konverterfunktionen werden in die Datei Include/converters.h eingetragen. Die Datei wird vom Präprozessor gelesen, bevor .c-Dateien übersetzt werden. Dies ist ein Auszug
/*[converter]
##### Default converters #####
"s": str -> const char *res;
"s*": [str, bytes, bytearray, rw_buffer] -> Py_buffer &res;
[...]
"es#": str -> (const char *res_encoding, char **res, Py_ssize_t *res_length);
[...]
##### Custom converters #####
path_converter: [str, bytes, int] -> path_t &res;
OS_STAT_DIR_FD_CONVERTER: [int, None] -> int res;
[converter_end]*/
Konverter werden nach ihrem Namen, den Python-Eingabetypen und den C-Ausgabetypen spezifiziert. Standardkonverter müssen angeführte Namen haben, benutzerdefinierte Konverter müssen reguläre Namen haben. Ein Python-Typ wird durch seinen Namen angegeben. Wenn eine Funktion mehrere Python-Typen akzeptiert, wird die Menge in Listenform geschrieben.
Da die Standardkonverter mehrere implizite Rückgabewerte haben können, werden die C-Ausgabetypen nach folgender Konvention geschrieben
Der Hauptrückgabewert muss res genannt werden. Dies ist ein Platzhalter für den tatsächlichen Variablennamen, der später in der DSL angegeben wird. Zusätzliche implizite Rückgabewerte müssen mit res_ präfixiert sein.
Standardmäßig werden die Variablen per Wert an die Implementierungsfunktion übergeben. Wenn stattdessen die Adresse übergeben werden soll, muss res mit einem Ampersand präfixiert werden.
Zusätzliche Deklarationen können in .c-Dateien platziert werden. Duplizierte Deklarationen sind zulässig, solange die Funktionstypen identisch sind.
Es wird empfohlen, benutzerdefinierte Konvertertypen direkt über der Konverterfunktionsdefinition ein zweites Mal zu deklarieren. Der Präprozessor fängt dann alle Abweichungen zwischen den Deklarationen ab.
Um die Komplexität der Konverter überschaubar zu halten, wird PY_SSIZE_T_CLEAN veraltet sein und Py_ssize_t wird für alle Längenargumente angenommen.
TBD: Eine Liste von Fantasietypen wie rw_buffer erstellen.
Funktionsspezifikationen
Schlüsselwortargumente
Dieses Beispiel enthält die Definition von os.stat. Die einzelnen Abschnitte werden im Detail erläutert. Grammatisch besteht der gesamte define-Block aus einer Funktionsspezifikation und einem Ausgabebereich. Die Funktionsspezifikation besteht wiederum aus einem Deklarationsbereich, einem optionalen C-Deklarationsbereich und einem optionalen Aufräumcodebereich. Abschnitte innerhalb der Funktionsspezifikation werden im Yacc-Stil durch ‚%%‘ getrennt
/*[define posix_stat]
def os.stat(path: path_converter, *, dir_fd: OS_STAT_DIR_FD_CONVERTER = None,
follow_symlinks: "p" = True) -> os.stat_result: pass
%%
path_t path = PATH_T_INITIALIZE("stat", 0, 1);
int dir_fd = DEFAULT_DIR_FD;
int follow_symlinks = 1;
%%
path_cleanup(&path);
[define_end]*/
<literal C output>
/*[define_output_end]*/
Define-Block
Der Funktionsspezifikationsblock beginnt mit einem /*[define Token, gefolgt von einem optionalen C-Funktionsnamen, gefolgt von einer schließenden Klammer. Wenn der C-Funktionsname nicht angegeben ist, wird er aus dem Deklarationsnamen generiert. Im Beispiel würde das Weglassen des Namens posix_stat zu einem C-Funktionsnamen von os_stat führen.
Deklaration
Die erforderliche Deklaration ist (fast) eine gültige Python-Funktionsdefinition. Das ‚def‘-Schlüsselwort und der Funktionskörper sind redundant, aber der Autor dieser PEP findet die Definition lesbarer, wenn sie vorhanden sind.
Der Funktionsname kann ein Pfad anstelle eines einfachen Bezeichners sein. Jedes Argument ist mit dem Namen der Konverterfunktion annotiert, die darauf angewendet wird.
Standardwerte werden auf die übliche Python-Weise angegeben und können beliebige gültige Python-Ausdrücke sein.
Der Rückgabewert kann ein beliebiger Python-Ausdruck sein. Normalerweise ist dies der Name eines Objekts, aber alternative Rückgabewerte könnten in Listenform angegeben werden.
C-Deklarationen
Dieser optionale Abschnitt enthält C-Variablendeklarationen. Da die Konverterfunktionen zuvor deklariert wurden, kann der Präprozessor die Deklarationen typsicher prüfen.
Aufräumen
Der optionale Aufräumabschnitt enthält literalen C-Code, der unverändert nach der Implementierungsfunktion eingefügt wird.
Ausgabe
Der Ausgabebereich enthält den vom Präprozessor ausgegebenen Code.
Positionsgebundene Argumente
Funktionen, die keine Schlüsselwortargumente akzeptieren, werden durch die Anwesenheit des speziellen Parameters slash gekennzeichnet
/*[define stat_float_times]
def os.stat_float_times(/, newval: "i") -> os.stat_result: pass
%%
int newval = -1;
[define_end]*/
Der Präprozessor übersetzt diese Definition in einen PyArg_ParseTuple()-Aufruf. Alle Argumente rechts vom Schrägstrich sind optionale Argumente.
Optionale Argumente links und rechts
Einige Legacy-Funktionen enthalten optionale Argumentgruppen sowohl links als auch rechts von einem zentralen Parameter. Es ist fraglich, ob ein neues Werkzeug solche Funktionen unterstützen sollte. Der Vollständigkeit halber ist hier die vorgeschlagene Syntax
/*[define]
def curses.window.addch(y: "i", x: "i", ch: "O", attr: "l") -> None: pass
where groups = [[ch], [ch, attr], [y, x, ch], [y, x, ch, attr]]
[define_end]*/
Hier ist ch der zentrale Parameter, attr kann optional rechts hinzugefügt werden, und die Gruppe [y, x] kann optional links hinzugefügt werden.
Im Wesentlichen lautet die Regel, dass alle geordneten Kombinationen des zentralen Parameters und der optionalen Gruppen möglich sein müssen, sodass keine zwei Kombinationen die gleiche Länge haben.
Dies wird prägnant ausgedrückt, indem der zentrale Parameter zuerst in der Liste aufgeführt und anschließend die optionalen Argumentgruppen links und rechts hinzugefügt werden.
Flexibilität bei der Formatierung
Wenn das obige os.stat-Beispiel als zu kompakt betrachtet wird, kann es einfach so formatiert werden
/*[define posix_stat]
def os.stat(path: path_converter,
*,
dir_fd: OS_STAT_DIR_FD_CONVERTER = None,
follow_symlinks: "p" = True)
-> os.stat_result: pass
%%
path_t path = PATH_T_INITIALIZE("stat", 0, 1);
int dir_fd = DEFAULT_DIR_FD;
int follow_symlinks = 1;
%%
path_cleanup(&path);
[define_end]*/
<literal C output>
/*[define_output_end]*/
Vorteile einer kompakten Notation
Die Vorteile einer prägnanten Notation sind besonders offensichtlich, wenn eine große Anzahl von Parametern beteiligt ist. Der Argument-Parsing-Teil von _posixsubprocess.fork_exec wird vollständig durch diese Definition spezifiziert
/*[define subprocess_fork_exec]
def _posixsubprocess.fork_exec(
process_args: "O", executable_list: "O",
close_fds: "p", py_fds_to_keep: "O",
cwd_obj: "O", env_list: "O",
p2cread: "i", p2cwrite: "i", c2pread: "i", c2pwrite: "i",
errread: "i", errwrite: "i", errpipe_read: "i", errpipe_write: "i",
restore_signals: "i", call_setsid: "i", preexec_fn: "i", /) -> int: pass
[define_end]*/
Beachten Sie, dass das preprocess-Tool derzeit einen redundanten C-Deklarationsabschnitt für dieses Beispiel ausgibt, sodass die Ausgabe länger ist als nötig.
Einfache Validierung der Definition
Wie kann ein unerfahrener Benutzer eine Definition wie os.stat validieren? Einfach, indem er os.stat in os_stat ändert, fehlende Konverter definiert und die Definition in den interaktiven Python-Interpreter einfügt!
Tatsächlich könnte ein converters.py-Modul aus converters.h generiert werden.
Referenzimplementierung
Eine Referenzimplementierung ist unter issue 16612 verfügbar. Da diese PEP unter Zeitdruck geschrieben wurde und der Autor mit der PLY-Toolchain nicht vertraut ist, ist die Software in Standard ML geschrieben und verwendet die ml-yacc/ml-lex-Toolchain.
Die Grammatik ist konfliktfrei und in ml-yacc lesbarer BNF-Form verfügbar.
Zwei Werkzeuge sind verfügbar
- printsemant liest einen Konverter-Header und eine .c-Datei und gibt den semantisch geprüften Parse-Baum auf stdout aus.
- preprocess liest einen Konverter-Header und eine .c-Datei und gibt die vorverarbeitete .c-Datei auf stdout aus.
Bekannte Mängel
- Der Python-‚test‘-Ausdruck wird nicht semantisch geprüft. Die Syntax wird jedoch geprüft, da sie Teil der Grammatik ist.
- Der Lexer verarbeitet keine dreifach zitierten Zeichenketten.
- C-Deklarationen werden primitiv geparst. Die endgültige Implementierung sollte ‚declarator‘ und ‚init-declarator‘ aus der C-Grammatik verwenden.
- Das preprocess-Tool gibt keinen Code für den Fall der linken und rechten optionalen Argumente aus. Das printsemant-Tool kann mit diesem Fall umgehen.
- Da das preprocess-Tool die Ausgabe aus dem Parse-Baum generiert, geht die ursprüngliche Einrückung des define-Blocks verloren.
Grammatik
TBD: Die Grammatik existiert in ml-yacc lesbarer Form, sollte aber wahrscheinlich hier in EBNF-Notation aufgenommen werden.
Vergleich mit PEP 436
Der Autor dieser PEP hat folgende Bedenken bezüglich der in PEP 436 vorgeschlagenen DSL
- Die Whitespace-empfindliche Konfigurationsdatei-ähnliche Syntax wirkt fehl am Platz in einer C-Datei.
- Die Struktur der Funktionsdefinition geht in den pro Parameter spezifizierten Angaben verloren. Schlüsselwörter wie positionsgebunden, erforderlich und nur-Schlüsselwort sind über zu viele verschiedene Stellen verstreut.
Im Gegensatz dazu kann in der alternativen DSL die Struktur der Funktionsdefinition auf einen Blick erfasst werden.
- Die PEP 436 DSL hat 14 dokumentierte Flags und mindestens ein undokumentiertes (allow_fd) Flag. Die Ermittlung, welche der 2**15 möglichen Kombinationen gültig sind, stellt eine unnötige Belastung für den Benutzer dar.
Erfahrungen mit den PEP 3118 Buffer-Flags haben gezeigt, dass das Sortieren (und erschöpfende Testen!) gültiger Kombinationen eine äußerst mühsame Aufgabe ist. Die PEP 3118 Flags sind immer noch nicht gut verstanden von vielen Leuten.
Im Gegensatz dazu gibt es bei der alternativen DSL eine zentrale Datei Include/converters.h, die schnell nach dem gewünschten Konverter durchsucht werden kann. Viele der Konverter sind bereits bekannt, vielleicht sogar auswendig gelernt von Leuten (aufgrund häufiger Nutzung).
- Die PEP 436 DSL lässt zu viel Freiheit. Typen können anscheinend weggelassen werden, der Präprozessor akzeptiert (und ignoriert) unbekannte Schlüsselwörter, und das Hinzufügen von Leerzeichen nach einem Docstring führt manchmal zu einem Assertionsfehler.
Die alternative DSL hingegen lässt keine solchen Freiheiten zu. Das Weglassen von Konverter- oder Rückgabewert-Annotationen ist eindeutig ein Syntaxfehler. Die LALR(1)-Grammatik ist eindeutig und für die vollständige Übersetzungseinheit spezifiziert.
Urheberrecht
Dieses Dokument unterliegt der Open Publication License.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0437.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT