PEP 580 – Das C-Aufrufprotokoll
- Autor:
- Jeroen Demeyer <J.Demeyer at UGent.be>
- BDFL-Delegate:
- Petr Viktorin
- Status:
- Abgelehnt
- Typ:
- Standards Track
- Erstellt:
- 14-Jun-2018
- Python-Version:
- 3.8
- Post-History:
- 20-Jun-2018, 22-Jun-2018, 16-Jul-2018
Inhaltsverzeichnis
Ablehnungsbescheid
Diese PEP wird zugunsten von PEP 590 abgelehnt, die eine einfachere öffentliche C-API für aufrufbare Objekte vorschlägt.
Zusammenfassung
Es wird ein neues „C-Aufrufprotokoll“ vorgeschlagen. Es ist für Klassen gedacht, die Funktionen oder Methoden repräsentieren und schnelles Aufrufen implementieren müssen. Das Ziel ist es, alle bestehenden Optimierungen für eingebaute Funktionen auf beliebige Erweiterungstypen zu verallgemeinern.
In der Referenzimplementierung wird dieses neue Protokoll für die bestehenden Klassen builtin_function_or_method und method_descriptor verwendet. In Zukunft können jedoch weitere Klassen es implementieren.
HINWEIS: Diese PEP befasst sich nur mit der Python/C-API, sie wirkt sich nicht auf die Python-Sprache oder die Standardbibliothek aus.
Motivation
Die Standard-Funktions-/Methodenklassen builtin_function_or_method und method_descriptor ermöglichen sehr effizientes Aufrufen von C-Code. Sie sind jedoch nicht unterklassifizierbar, was sie für viele Anwendungen ungeeignet macht: Sie bieten beispielsweise eingeschränkte Introspektionsunterstützung (Signaturen nur über __text_signature__, kein beliebiges __qualname__, kein inspect.getfile()). Es ist auch nicht möglich, zusätzliche Daten zu speichern, um etwas wie functools.partial oder functools.lru_cache zu implementieren. Es gibt also viele Gründe, warum Benutzer benutzerdefinierte Funktions-/Methodenklassen (im Sinne von Duck-Typing) in C implementieren möchten. Leider sind solche benutzerdefinierten Klassen zwangsläufig langsamer als die standardmäßigen CPython-Funktionsklassen: Der Bytecode-Interpreter verfügt über verschiedene Optimierungen, die spezifisch für Instanzen von builtin_function_or_method, method_descriptor, method und function sind.
Diese PEP ermöglicht auch die Vereinfachung bestehenden Codes: Überprüfungen auf builtin_function_or_method und method_descriptor könnten durch einfaches Überprüfen und Verwenden des C-Aufrufprotokolls ersetzt werden. Zukünftige PEPs können das C-Aufrufprotokoll für weitere Klassen implementieren und so noch weitere Vereinfachungen ermöglichen.
Wir gestalten das C-Aufrufprotokoll auch so, dass es in Zukunft leicht um neue Funktionen erweitert werden kann.
Weitere Hintergründe und Motivation finden Sie in PEP 579.
Übersicht
Derzeit verfügt CPython über mehrere Optimierungen für schnelles Aufrufen für einige spezifische Funktionsklassen. Ein gutes Beispiel ist die Implementierung des Op-Codes CALL_FUNCTION, der die folgende Struktur hat (siehe den tatsächlichen Code)
if (PyCFunction_Check(func)) {
return _PyCFunction_FastCallKeywords(func, stack, nargs, kwnames);
}
else if (Py_TYPE(func) == &PyMethodDescr_Type) {
return _PyMethodDescr_FastCallKeywords(func, stack, nargs, kwnames);
}
else {
if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL) {
/* ... */
}
if (PyFunction_Check(func)) {
return _PyFunction_FastCallKeywords(func, stack, nargs, kwnames);
}
else {
return _PyObject_FastCallKeywords(func, stack, nargs, kwnames);
}
}
Das Aufrufen von Instanzen dieser speziell behandelten Klassen über den tp_call-Slot ist langsamer als die Verwendung der Optimierungen. Die Grundidee dieser PEP ist es, solche Optimierungen für Benutzer-C-Code zu ermöglichen, sowohl als Aufrufer als auch als Aufgerufener.
Die bestehende Klasse builtin_function_or_method und einige andere verwenden eine PyMethodDef-Struktur zur Beschreibung der zugrundeliegenden C-Funktion und ihrer Signatur. Die erste konkrete Änderung ist, dass diese durch eine neue Struktur PyCCallDef ersetzt wird. Diese speichert einige der gleichen Informationen wie eine PyMethodDef, jedoch mit einer wichtigen Ergänzung: dem „Elternteil“ der Funktion (der Klasse oder des Moduls, in dem sie definiert ist). Beachten Sie, dass PyMethodDef-Arrays weiterhin zum Erstellen von Funktionen/Methoden verwendet werden, aber nicht mehr zum Aufrufen.
Zweitens möchten wir, dass jede Klasse eine solche PyCCallDef zur Optimierung von Aufrufen verwenden kann, daher erhält die PyTypeObject-Struktur ein Feld tp_ccalloffset, das einen Offset zu einer PyCCallDef * in der Objektstruktur angibt, und ein Flag Py_TPFLAGS_HAVE_CCALL, das anzeigt, dass tp_ccalloffset gültig ist.
Drittens, da wir auch mit ungebundenen und gebundenen Methoden (im Gegensatz zu reinen Funktionen) effizient umgehen wollen, müssen wir __self__ im Protokoll behandeln: nach dem PyCCallDef * in der Objektstruktur folgt ein Feld PyObject *self. Diese beiden Felder zusammen werden als PyCCallRoot-Struktur bezeichnet.
Das neue Protokoll zum effizienten Aufrufen von Objekten unter Verwendung dieser neuen Strukturen wird als „C-Aufrufprotokoll“ bezeichnet.
HINWEIS: In dieser PEP beziehen sich die Begriffe „ungebundene Methode“ und „gebundene Methode“ auf ein generisches Verhalten und nicht auf spezifische Klassen. Eine ungebundene Methode wird beispielsweise nach Anwendung von __get__ in eine gebundene Methode umgewandelt.
Neue Datenstrukturen
Die PyTypeObject-Struktur erhält ein neues Feld Py_ssize_t tp_ccalloffset und ein neues Flag Py_TPFLAGS_HAVE_CCALL. Wenn dieses Flag gesetzt ist, wird angenommen, dass tp_ccalloffset ein gültiger Offset innerhalb der Objektstruktur ist (ähnlich wie tp_dictoffset und tp_weaklistoffset). Es muss eine strikt positive ganze Zahl sein. An diesem Offset befindet sich eine PyCCallRoot-Struktur.
typedef struct {
const PyCCallDef *cr_ccall;
PyObject *cr_self; /* __self__ argument for methods */
} PyCCallRoot;
Die PyCCallDef-Struktur enthält alles Notwendige, um zu beschreiben, wie die Funktion aufgerufen werden kann.
typedef struct {
uint32_t cc_flags;
PyCFunc cc_func; /* C function to call */
PyObject *cc_parent; /* class or module */
} PyCCallDef;
Der Grund für die Platzierung von __self__ außerhalb von PyCCallDef ist, dass PyCCallDef nicht nach der Erstellung der Funktion geändert werden soll. Eine einzige PyCCallDef kann von einer ungebundenen Methode und mehreren gebundenen Methoden gemeinsam genutzt werden. Dies würde nicht funktionieren, wenn wir __self__ in diese Struktur aufnehmen würden.
HINWEIS: Im Gegensatz zu tp_dictoffset erlauben wir keine negativen Zahlen für tp_ccalloffset, um vom Ende zu zählen. Dafür scheint kein Anwendungsfall zu existieren und es würde die Implementierung nur verkomplizieren.
Oberklasse
Das Feld cc_parent (zugegriffen z. B. durch einen Deskriptor __parent__ oder __objclass__ aus Python-Code) kann jedes Python-Objekt oder NULL sein. Benutzerdefinierte Klassen können cc_parent frei nach Belieben setzen. Es wird nur vom C-Aufrufprotokoll verwendet, wenn das Flag CCALL_OBJCLASS gesetzt ist.
Für Methoden von Erweiterungstypen zeigt cc_parent auf die Klasse, die die Methode definiert (was eine Oberklasse von type(self) sein kann). Dies ist derzeit schwierig aus dem Code einer Methode abzurufen. Zukünftig kann dies verwendet werden, um über die definierende Klasse auf den Modulstatus zuzugreifen. Siehe die Begründung für PEP 573 für Details.
Wenn das Flag CCALL_OBJCLASS gesetzt ist (wie es bei Methoden von Erweiterungstypen der Fall sein wird), wird cc_parent für Typüberprüfungen wie die folgende verwendet:
>>> list.append({}, "x")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: descriptor 'append' requires a 'list' object but received a 'dict'
Für Funktionen eines Moduls wird cc_parent auf das Modul gesetzt. Derzeit ist dies genau dasselbe wie __self__. Die Verwendung von __self__ für das Modul ist jedoch eine Eigenart der aktuellen Implementierung: Zukünftig möchten wir Funktionen zulassen, die __self__ auf normale Weise verwenden, um Methoden zu implementieren. Solche Funktionen können weiterhin stattdessen cc_parent verwenden, um auf das Modul zu verweisen.
Die Oberklasse würde typischerweise auch zur Implementierung von __qualname__ verwendet. Die neue C-API-Funktion PyCCall_GenericGetQualname() tut genau das.
Verwendung von tp_print
Wir schlagen vor, das bestehende ungenutzte Feld tp_print durch tp_ccalloffset zu ersetzen. Da Py_TPFLAGS_HAVE_CCALL *nicht* zu Py_TPFLAGS_DEFAULT hinzugefügt würde, ist dies vollständig abwärtskompatibel für bestehende Erweiterungsmodule, die tp_print setzen. Es bedeutet auch, dass wir verlangen können, dass tp_ccalloffset ein gültiger Offset ist, wenn Py_TPFLAGS_HAVE_CCALL angegeben ist: wir müssen nicht tp_ccalloffset != 0 überprüfen. In zukünftigen Python-Versionen könnten wir entscheiden, dass tp_print bedingungslos zu tp_ccalloffset wird, das Flag Py_TPFLAGS_HAVE_CCALL fallen lassen und stattdessen tp_ccalloffset != 0 prüfen.
HINWEIS: Das genaue Layout von PyTypeObject ist kein Teil der stabilen ABI. Daher sollte die Änderung des Feldes tp_print von einem printfunc (ein Funktionszeiger) zu einem Py_ssize_t kein Problem darstellen, auch wenn dies das Speicherlayout der PyTypeObject-Struktur verändert. Darüber hinaus ist auf allen Systemen, für die Binärdateien üblicherweise erstellt werden (Windows, Linux, macOS), die Größe von printfunc und Py_ssize_t gleich, sodass das Problem der binären Kompatibilität ohnehin nicht auftreten wird.
Das C-Aufrufprotokoll
Wir sagen, dass eine Klasse das C-Aufrufprotokoll implementiert, wenn sie das Flag Py_TPFLAGS_HAVE_CCALL gesetzt hat (wie oben erklärt, muss sie dann tp_ccalloffset > 0 setzen). Eine solche Klasse muss __call__ wie in diesem Abschnitt beschrieben implementieren (in der Praxis bedeutet dies lediglich das Setzen von tp_call auf PyCCall_Call).
Das Feld cc_func ist ein C-Funktionszeiger, der die gleiche Rolle spielt wie das bestehende Feld ml_meth von PyMethodDef. Seine genaue Signatur hängt von Flags ab. Die Teilmenge der Flags, die die Signatur von cc_func beeinflussen, wird durch die Bitmaske CCALL_SIGNATURE gegeben. Nachfolgend sind die möglichen Werte für cc_flags & CCALL_SIGNATURE zusammen mit den Argumenten aufgeführt, die die C-Funktion akzeptiert. Der Rückgabewert ist immer PyObject *. Die folgenden sind analog zu den bestehenden PyMethodDef-Signatur-Flags:
CCALL_VARARGS:cc_func(PyObject *self, PyObject *args)CCALL_VARARGS | CCALL_KEYWORDS:cc_func(PyObject *self, PyObject *args, PyObject *kwds)(kwdsist entwederNULLoder ein Dict; dieses Dict darf vom Aufgerufenen nicht geändert werden)CCALL_FASTCALL:cc_func(PyObject *self, PyObject *const *args, Py_ssize_t nargs)CCALL_FASTCALL | CCALL_KEYWORDS:cc_func(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)(kwnamesist entwederNULLoder ein nicht-leeres Tupel von Keyword-Namen). In letzterem Fall werden die Keyword-Werte imargs-Array gespeichert, beginnend beiargs[nargs].CCALL_NOARGS:cc_func(PyObject *self, PyObject *unused)(zweites Argument ist immerNULL)CCALL_O:cc_func(PyObject *self, PyObject *arg)
Das Flag CCALL_DEFARG kann mit jedem dieser Flags kombiniert werden. Wenn dies der Fall ist, nimmt die C-Funktion ein zusätzliches Argument als erstes Argument vor self entgegen, nämlich einen konstanten Zeiger auf die PyCCallDef-Struktur, die für diesen Aufruf verwendet wird. Zum Beispiel haben wir die folgende Signatur:
CCALL_DEFARG | CCALL_VARARGS:cc_func(const PyCCallDef *def, PyObject *self, PyObject *args)
Eine Ausnahme bildet CCALL_DEFARG | CCALL_NOARGS: das Argument unused wird weggelassen, sodass die Signatur lautet:
CCALL_DEFARG | CCALL_NOARGS:cc_func(const PyCCallDef *def, PyObject *self)
HINWEIS: Im Gegensatz zu den bestehenden METH_...-Flags stellen die CCALL_...-Konstanten nicht unbedingt einzelne Bits dar. Daher ist die Überprüfung if (cc_flags & CCALL_VARARGS) keine gültige Methode zur Überprüfung der Signatur. Es gibt auch keine Garantien für die binäre Kompatibilität dieser Flags zwischen Python-Versionen. Dies ermöglicht der Implementierung, die effizientesten numerischen Werte für die Flags zu wählen. In der Referenzimplementierung bilden die gültigen Werte für cc_flags & CCALL_SIGNATURE genau das Intervall [0, ..., 11]. Das bedeutet, dass der Compiler eine switch-Anweisung für diese Fälle mithilfe eines berechneten Gotos leicht optimieren kann.
Überprüfung von __objclass__
Wenn das Flag CCALL_OBJCLASS gesetzt ist und cr_self NULL ist (dies ist bei ungebundenen Methoden von Erweiterungstypen der Fall), dann wird eine Typüberprüfung durchgeführt: die Funktion muss mit mindestens einem Positionsargument aufgerufen werden und das erste (typischerweise self genannt) muss eine Instanz von cc_parent (welche eine Klasse sein muss) sein. Andernfalls wird ein TypeError ausgelöst.
Selbstreferenz-Slicing
Wenn cr_self nicht NULL ist oder wenn das Flag CCALL_SELFARG in cc_flags nicht gesetzt ist, dann ist das als self übergebene Argument einfach cr_self.
Wenn cr_self NULL ist und das Flag CCALL_SELFARG gesetzt ist, dann wird das erste Positionsargument von args entfernt und stattdessen als self-Argument an die C-Funktion übergeben. Effektiv wird das erste Positionsargument als __self__ behandelt. Wenn keine Positionsargumente vorhanden sind, wird ein TypeError ausgelöst.
Dieser Vorgang wird als „Selbstreferenz-Slicing“ bezeichnet und eine Funktion soll Selbstreferenz-Slicing haben, wenn cr_self NULL ist und CCALL_SELFARG gesetzt ist.
Beachten Sie, dass eine CCALL_NOARGS-Funktion mit Selbstreferenz-Slicing effektiv ein Argument hat, nämlich self. Analog dazu hat eine CCALL_O-Funktion mit Selbstreferenz-Slicing zwei Argumente.
Deskriptorverhalten
Klassen, die das C-Aufrufprotokoll unterstützen, müssen das Deskriptorprotokoll auf eine bestimmte Weise implementieren.
Dies ist für eine effiziente Implementierung von gebundenen Methoden erforderlich: Wenn anderer Code Annahmen darüber treffen kann, was __get__ tut, ermöglicht dies Optimierungen, die sonst nicht möglich wären. Insbesondere möchten wir die gemeinsame Nutzung der PyCCallDef-Struktur zwischen gebundenen und ungebundenen Methoden ermöglichen. Wir benötigen auch eine korrekte Implementierung von _PyObject_GetMethod, das von der Optimierung LOAD_METHOD/CALL_METHOD verwendet wird.
Zunächst einmal, wenn func das C-Aufrufprotokoll unterstützt, dann dürfen func.__set__ und func.__delete__ nicht implementiert sein.
Zweitens muss func.__get__ wie folgt verhalten:
- Wenn
cr_selfnicht NULL ist, dann muss__get__ein No-Op sein in dem Sinne, dassfunc.__get__(obj, cls)(*args, **kwds)sich exakt gleich verhält wiefunc(*args, **kwds). Es ist auch erlaubt, dass__get__überhaupt nicht implementiert ist. - Wenn
cr_selfNULL ist, dann mussfunc.__get__(obj, cls)(*args, **kwds)(mitobjnicht None) äquivalent zufunc(obj, *args, **kwds)sein. Insbesondere muss__get__in diesem Fall implementiert sein. Dies steht in keinem Zusammenhang mit Selbstreferenz-Slicing:objkann alsself-Argument an die C-Funktion übergeben werden oder es kann das erste Positionsargument sein. - Wenn
cr_selfNULL ist, dann mussfunc.__get__(None, cls)(*args, **kwds)äquivalent zufunc(*args, **kwds)sein.
Es gibt keine Einschränkungen für das Objekt func.__get__(obj, cls). Letzteres muss zum Beispiel nicht das C-Aufrufprotokoll implementieren. Wir geben nur an, was func.__get__(obj, cls).__call__ tut.
Für Klassen, die __self__ und __get__ überhaupt nicht interessieren, ist die einfachste Lösung, cr_self = Py_None (oder irgendeinen anderen Wert ungleich NULL) zuzuweisen.
Das __name__ Attribut
Das C-Aufrufprotokoll verlangt, dass die Funktion ein Attribut __name__ vom Typ str (keine Unterklasse) besitzt.
Darüber hinaus muss das Objekt, das von __name__ zurückgegeben wird, irgendwo gespeichert werden; es darf kein temporäres Objekt sein. Dies ist erforderlich, da PyEval_GetFuncName eine geliehene Referenz auf das Attribut __name__ verwendet (siehe auch [2]).
Generische API-Funktionen
Dieser Abschnitt listet die neuen öffentlichen API-Funktionen oder Makros auf, die sich mit dem C-Aufrufprotokoll befassen.
int PyCCall_Check(PyObject *op): gibt wahr zurück, wennopdas C-Aufrufprotokoll implementiert.
Alle nachfolgenden Funktionen und Makros gelten für jede Instanz, die das C-Aufrufprotokoll unterstützt. Mit anderen Worten, PyCCall_Check(func) muss wahr sein.
PyObject *PyCCall_Call(PyObject *func, PyObject *args, PyObject *kwds): ruftfuncmit den Positionsargumentenargsund den Schlüsselwortargumentenkwdsauf (kwdskann NULL sein). Diese Funktion ist dafür gedacht, in dentp_call-Slot eingefügt zu werden.PyObject *PyCCall_FastCall(PyObject *func, PyObject *const *args, Py_ssize_t nargs, PyObject *kwds): ruftfuncmitnargsPositionsargumenten, die vonargs[0]bisargs[nargs-1]gegeben sind, auf. Der Parameterkwdskann NULL sein (keine Schlüsselwortargumente), ein Dict mitname:value-Items oder ein Tupel mit Schlüsselwortnamen. Im letzteren Fall werden die Schlüsselwortwerte imargs-Array gespeichert, beginnend beiargs[nargs].
Makros zum Zugriff auf die Strukturen PyCCallRoot und PyCCallDef
const PyCCallRoot *PyCCall_CCALLROOT(PyObject *func): Zeiger auf diePyCCallRoot-Struktur innerhalb vonfunc.const PyCCallDef *PyCCall_CCALLDEF(PyObject *func): Abkürzung fürPyCCall_CCALLROOT(func)->cr_ccall.uint32_t PyCCall_FLAGS(PyObject *func): Abkürzung fürPyCCall_CCALLROOT(func)->cr_ccall->cc_flags.PyObject *PyCCall_SELF(PyOject *func): Abkürzung fürPyCCall_CCALLROOT(func)->cr_self.
Generische Getter, die in das tp_getset-Array eingefügt werden sollen
PyObject *PyCCall_GenericGetParent(PyObject *func, void *closure): gibtcc_parentzurück. LöstAttributeErroraus, wenncc_parentNULL ist.PyObject *PyCCall_GenericGetQualname(PyObject *func, void *closure): gibt einen String zurück, der als__qualname__verwendet werden kann. Dies verwendet das__qualname__voncc_parent, wenn möglich. Es verwendet auch das Attribut__name__.
Profiling
Die Profiling-Events c_call, c_return und c_exception werden nur beim Aufrufen von tatsächlichen Instanzen von builtin_function_or_method oder method_descriptor generiert. Dies geschieht aus Gründen der Einfachheit und auch aus Gründen der Abwärtskompatibilität (damit die Profil-Funktion keine Objekte erhält, die sie nicht erkennt). In einer zukünftigen PEP könnten wir das C-Level-Profiling auf beliebige Klassen erweitern, die das C-Aufrufprotokoll implementieren.
Änderungen an eingebauten Funktionen und Methoden
Die Referenzimplementierung dieser PEP ändert die bestehenden Klassen builtin_function_or_method und method_descriptor so, dass sie das C-Aufrufprotokoll verwenden. Tatsächlich sind diese beiden Klassen fast verschmolzen: Die Implementierung wird sehr ähnlich, aber sie bleiben getrennte Klassen (hauptsächlich aus Gründen der Abwärtskompatibilität). Die Struktur PyCCallDef wird einfach als Teil der Objektstruktur gespeichert. Beide Klassen verwenden PyCFunctionObject als Objektstruktur. Dies ist das neue Layout für beide Klassen:
typedef struct {
PyObject_HEAD
PyCCallDef *m_ccall;
PyObject *m_self; /* Passed as 'self' arg to the C function */
PyCCallDef _ccalldef; /* Storage for m_ccall */
PyObject *m_name; /* __name__; str object (not NULL) */
PyObject *m_module; /* __module__; can be anything */
const char *m_doc; /* __text_signature__ and __doc__ */
PyObject *m_weakreflist; /* List of weak references */
} PyCFunctionObject;
Für Funktionen eines Moduls und für ungebundene Methoden von Erweiterungstypen zeigt m_ccall auf das Feld _ccalldef. Für gebundene Methoden zeigt m_ccall auf die PyCCallDef der ungebundenen Methode.
HINWEIS: Das neue Layout von method_descriptor ändert es so, dass es nicht mehr mit PyDescr_COMMON beginnt. Dies ist rein ein Implementierungsdetail und sollte wenige (wenn überhaupt) Kompatibilitätsprobleme verursachen.
C-API-Funktionen
Die folgende Funktion wird hinzugefügt (auch zur stable ABI)
PyObject * PyCFunction_ClsNew(PyTypeObject *cls, PyMethodDef *ml, PyObject *self, PyObject *module, PyObject *parent): Erstellt ein neues Objekt mit der ObjektstrukturPyCFunctionObjectund der Klassecls. Die Einträge derPyMethodDefStruktur werden verwendet, um das neue Objekt zu konstruieren, aber der Zeiger auf diePyMethodDefStruktur wird nicht gespeichert. Die Flags für das C-Aufrufprotokoll werden automatisch in Bezug aufml->ml_flags,selfundparentermittelt.
Die bestehenden Funktionen PyCFunction_New, PyCFunction_NewEx und PyDescr_NewMethod werden in Bezug auf PyCFunction_ClsNew implementiert.
Die undokumentierten Funktionen PyCFunction_GetFlags und PyCFunction_GET_FLAGS sind veraltet. Sie werden immer noch künstlich unterstützt, indem die ursprünglichen METH_... Flags in einem Bitfeld innerhalb von cc_flags gespeichert werden. Obwohl PyCFunction_GetFlags technisch Teil der stable ABI ist, ist es höchst unwahrscheinlich, dass sie so verwendet wird: Erstens ist sie nicht einmal dokumentiert. Zweitens ist das Flag METH_FASTCALL kein Teil der stable ABI, aber es ist sehr verbreitet (wegen Argument Clinic). Wenn man also METH_FASTCALL nicht unterstützen kann, ist es schwer, einen Anwendungsfall für PyCFunction_GetFlags vorzustellen. Die Tatsache, dass PyCFunction_GET_FLAGS und PyCFunction_GetFlags von CPython außerhalb von Objects/call.c gar nicht verwendet werden, zeigt weiter, dass diese Funktionen nicht besonders nützlich sind.
Vererbung
Erweiterungstypen erben das Typ-Flag Py_TPFLAGS_HAVE_CCALL und den Wert tp_ccalloffset von der Basisklasse, vorausgesetzt, sie implementieren tp_call und tp_descr_get auf die gleiche Weise wie die Basisklasse. Heap-Typen erben niemals das C-Aufrufprotokoll, da dies nicht sicher wäre (Heap-Typen können dynamisch geändert werden).
Performance
Dieser PEP sollte keine Auswirkungen auf die Leistung bestehenden Codes haben (weder positiv noch negativ). Er soll das Schreiben effizienten neuen Codes ermöglichen, nicht bestehenden Code schneller machen.
Hier sind ein paar Links zur Mailingliste python-dev, wo Leistungsverbesserungen diskutiert werden
Stabile ABI
Die Funktion PyCFunction_ClsNew wird zur stable ABI hinzugefügt.
Keine der Funktionen, Strukturen oder Konstanten, die sich mit dem C-Aufrufprotokoll befassen, werden zur stable ABI hinzugefügt.
Dafür gibt es zwei Gründe: Erstens ist das nützlichste Merkmal des C-Aufrufprotokolls wahrscheinlich die Aufrufkonvention METH_FASTCALL. Da diese nicht einmal Teil der öffentlichen API ist (siehe auch PEP 579, Issue 6), wäre es seltsam, etwas anderes aus dem C-Aufrufprotokoll zur stable ABI hinzuzufügen.
Zweitens möchten wir, dass das C-Aufrufprotokoll in Zukunft erweiterbar ist. Indem wir nichts zur stable ABI hinzufügen, sind wir frei, dies ohne Einschränkungen zu tun.
Abwärtskompatibilität
Für die Python-Schnittstelle gibt es keinerlei Unterschiede, ebenso wenig für die dokumentierte C-API (im Sinne, dass alle Funktionen mit der gleichen Funktionalität unterstützt bleiben).
Die einzige potenzielle Bruchstelle ist C-Code, der auf die Interna von PyCFunctionObject und PyMethodDescrObject zugreift. Wir erwarten aufgrund dessen nur sehr wenige Probleme.
Begründung
Warum ist das besser als PEP 575?
Eine der Hauptbeschwerden von PEP 575 war, dass sie Funktionalität (das Aufruf- und Introspektionsprotokoll) mit der Klassenhierarchie koppelte: Eine Klasse konnte von den neuen Features nur profitieren, wenn sie eine Unterklasse von base_function war. Für bestehende Klassen kann dies schwierig sein, da sie andere Einschränkungen hinsichtlich des Layouts der C-Objektstruktur haben könnten, die von einer bestehenden Basisklasse oder Implementierungsdetails herrühren. Zum Beispiel kann functools.lru_cache PEP 575 nicht wie vorgesehen implementieren.
Es komplizierte auch die Implementierung gerade deswegen, weil sowohl in den Implementierungsdetails als auch in der Klassenhierarchie Änderungen erforderlich waren.
Der aktuelle PEP hat diese Probleme nicht.
Warum den Funktionszeiger in der Instanz speichern?
Die tatsächlichen Informationen, die zum Aufrufen eines Objekts benötigt werden, werden in der Instanz (in der PyCCallDef Struktur) anstatt in der Klasse gespeichert. Dies unterscheidet sich vom tp_call Slot oder früheren Versuchen, einen tp_fastcall Slot zu implementieren [1].
Der Hauptanwendungsfall sind eingebaute Funktionen und Methoden. Für diese hängt die aufzurufende C-Funktion von der Instanz ab.
Beachten Sie, dass das aktuelle Protokoll die Unterstützung für den Fall erleichtert, dass die gleiche C-Funktion für alle Instanzen aufgerufen wird: Verwenden Sie einfach eine einzige statische PyCCallDef Struktur für jede Instanz.
Warum CCALL_OBJCLASS?
Das Flag CCALL_OBJCLASS ist dazu gedacht, verschiedene Fälle zu unterstützen, in denen die Klasse eines self-Arguments überprüft werden muss, wie z.B.
>>> list.append({}, None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: append() requires a 'list' object but received a 'dict'
>>> list.__len__({})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: descriptor '__len__' requires a 'list' object but received a 'dict'
>>> float.__dict__["fromhex"](list, "0xff")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: descriptor 'fromhex' for type 'float' doesn't apply to type 'list'
In der Referenzimplementierung nutzt nur die erste dieser Anwendungen den neuen Code. Die anderen Beispiele zeigen, dass diese Art von Prüfungen an mehreren Stellen vorkommen, sodass es sinnvoll ist, generische Unterstützung dafür hinzuzufügen.
Warum CCALL_SELFARG?
Das Flag CCALL_SELFARG und das Konzept des Self-Slicings sind notwendig, um Methoden zu unterstützen: Die C-Funktion sollte nicht darauf angewiesen sein, ob sie als ungebundene Methode oder als gebundene Methode aufgerufen wird. In beiden Fällen sollte ein self-Argument vorhanden sein, und dies ist einfach das erste Positionsargument eines Aufrufs einer ungebundenen Methode.
Zum Beispiel ist list.append eine METH_O-Methode. Sowohl die Aufrufe list.append([], 42) als auch [].append(42) sollten zu dem C-Aufruf list_append([], 42) übersetzt werden.
Dank des vorgeschlagenen C-Aufrufprotokolls können wir dies so unterstützen, dass sowohl die ungebundene als auch die gebundene Methode eine PyCCallDef Struktur (mit dem gesetzten Flag CCALL_SELFARG) gemeinsam nutzen.
Daher hat CCALL_SELFARG zwei Vorteile: Es gibt keine zusätzliche Indirektionsebene für den Aufruf von Methoden, und die Erstellung von gebundenen Methoden erfordert nicht die Einrichtung einer PyCCallDef Struktur.
Ein weiterer kleiner Vorteil ist, dass wir die Fehlermeldungen für eine falsche Aufrufsignatur zwischen Python-Methoden und eingebauten Methoden einheitlicher gestalten könnten. Im folgenden Beispiel ist sich Python nicht sicher, ob eine Methode 1 oder 2 Argumente annimmt
>>> class List(list):
... def myappend(self, item):
... self.append(item)
>>> List().myappend(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: myappend() takes 2 positional arguments but 3 were given
>>> List().append(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: append() takes exactly one argument (2 given)
Es ist derzeit für PyCFunction_Call unmöglich, die tatsächliche Anzahl benutzer-sichtbarer Argumente zu kennen, da es zur Laufzeit nicht zwischen einer Funktion (ohne self-Argument) und einer gebundenen Methode (mit self-Argument) unterscheiden kann. Das Flag CCALL_SELFARG macht diesen Unterschied explizit.
Warum CCALL_DEFARG?
Das Flag CCALL_DEFARG gibt dem Aufgerufenen Zugriff auf das PyCCallDef *. Dafür gibt es verschiedene Anwendungsfälle
- Der Aufgerufene kann das Feld
cc_parentverwenden, was für PEP 573 nützlich ist. - Anwendungen können die
PyCCallDefStruktur frei mit benutzerdefinierten Feldern erweitern, auf die dann analog zugegriffen werden kann. - In dem Fall, dass die
PyCCallDefStruktur Teil der Objektstruktur ist (was z.B. für PyCFunctionObject zutrifft), kann ein geeigneter Offset vomPyCCallDefZeiger abgezogen werden, um einen Zeiger auf das aufrufbare Objekt zu erhalten, das diesePyCCallDefdefiniert.
Eine frühere Version dieses PEP definierte ein Flag CCALL_FUNCARG anstelle von CCALL_DEFARG, das das aufrufbare Objekt an den Aufgerufenen übergeben hätte. Dies hatte ähnliche Anwendungsfälle, aber es gab eine gewisse Mehrdeutigkeit bei gebundenen Methoden: Sollte das "aufrufbare Objekt" das gebundene Methodenobjekt oder die vom Methode umschlossene ursprüngliche Funktion sein? Durch die Übergabe des PyCCallDef * stattdessen entfällt diese Mehrdeutigkeit, da die gebundene Methode die PyCCallDef * von der umschlossenen Funktion verwendet.
Ersetzen von tp_print
Wir verwenden tp_print als tp_ccalloffset wieder, da dies externen Projekten das Zurückportieren des C-Aufrufprotokolls auf frühere Python-Versionen erleichtert. Insbesondere das Cython-Projekt hat Interesse daran gezeigt (siehe https://mail.python.org/pipermail/python-dev/2018-June/153927.html).
Alternative Vorschläge
PEP 576 ist ein alternativer Ansatz zur Lösung des gleichen Problems wie dieser PEP. Siehe https://mail.python.org/pipermail/python-dev/2018-July/154238.html für Kommentare zu den Unterschieden zwischen PEP 576 und PEP 580.
Diskussion
Links zu Threads auf der Mailingliste python-dev, auf denen dieser PEP diskutiert wurde
- https://mail.python.org/pipermail/python-dev/2018-June/153938.html
- https://mail.python.org/pipermail/python-dev/2018-June/153984.html
- https://mail.python.org/pipermail/python-dev/2018-July/154238.html
- https://mail.python.org/pipermail/python-dev/2018-July/154470.html
- https://mail.python.org/pipermail/python-dev/2018-July/154571.html
- https://mail.python.org/pipermail/python-dev/2018-September/155166.html
- https://mail.python.org/pipermail/python-dev/2018-October/155403.html
- https://mail.python.org/pipermail/python-dev/2019-March/156853.html
- https://mail.python.org/pipermail/python-dev/2019-March/156879.html
Referenzimplementierung
Die Referenzimplementierung finden Sie unter https://github.com/jdemeyer/cpython/tree/pep580
Als Beispiel für die Verwendung des C-Aufrufprotokolls implementiert der folgende Branch functools.lru_cache mit PEP 580: https://github.com/jdemeyer/cpython/tree/lru580
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0580.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT