PEP 579 – Refactoring von C-Funktionen und -Methoden
- Autor:
- Jeroen Demeyer <J.Demeyer at UGent.be>
- BDFL-Delegate:
- Petr Viktorin
- Status:
- Final
- Typ:
- Informational
- Erstellt:
- 04-Jun-2018
- Post-History:
- 20-Jun-2018
Inhaltsverzeichnis
- Genehmigungsmitteilung
- Zusammenfassung
- Probleme
- 1. Benennung
- 2. Nicht erweiterbar
- 3. cfunctions werden nicht zu Methoden
- 4. Semantik von inspect.isfunction
- 5. C-Funktionen sollten Zugriff auf das Funktions-Objekt haben
- 6. METH_FASTCALL ist privat und undokumentiert
- 7. Zulassen von nativen C-Argumenten
- 8. Komplexität
- 9. PyMethodDef ist zu begrenzt
- 10. Slot-Wrapper haben keine benutzerdefinierte Dokumentation
- 11. Statische Methoden und Klassenmethoden sollten aufrufbar sein
- Referenzen
- Urheberrecht
Genehmigungsmitteilung
Diese PEP beschreibt Design-Probleme, die in PEP 575, PEP 580, PEP 590 (und möglicherweise späteren Vorschlägen) behandelt werden.
Wie in PEP 1 erwähnt
Informative PEPs repräsentieren nicht notwendigerweise einen Konsens oder eine Empfehlung der Python-Community, daher steht es Benutzern und Implementierern frei, informative PEPs zu ignorieren oder ihren Ratschlägen zu folgen.
Obwohl es keinen Konsens darüber gibt, ob die Probleme oder die Lösungen in dieser PEP gültig sind, ist die Liste dennoch nützlich, um das weitere Design zu leiten.
Zusammenfassung
Diese Meta-PEP sammelt verschiedene Probleme mit CPythons bestehender Implementierung von eingebauten Funktionen (in C implementierte Funktionen) und Methoden.
Die Behebung all dieser Probleme ist zu viel für eine PEP, daher wird dies an andere Standards-Track-PEPs delegiert. Diese PEP gibt jedoch einige kurze Ideen für mögliche Korrekturen. Sie ist hauptsächlich dazu gedacht, eine Gesamtstrategie zu koordinieren. Zum Beispiel kann eine vorgeschlagene Lösung für die Behebung eines einzelnen Problems zu kompliziert erscheinen, aber sie kann die beste Gesamtlösung für mehrere Probleme sein.
Diese PEP ist rein informativ: sie impliziert nicht, dass alle Probleme letztendlich behoben werden, noch dass sie mit der hier vorgeschlagenen Lösung behoben werden.
Sie dient auch als Checkliste möglicher angeforderter Funktionen, um zu überprüfen, ob eine gegebene Korrektur die Implementierung dieser anderen Funktionen erschwert.
Die wichtigste vorgeschlagene Änderung ist der Ersatz von PyMethodDef durch eine neue Struktur PyCCallDef, die alles sammelt, was zum Aufrufen der Funktion/Methode benötigt wird. In der PyTypeObject-Struktur wird ein neues Feld tp_ccalloffset hinzugefügt, das einen Offset zu einem PyCCallDef * in der Objektstruktur angibt.
HINWEIS: Diese PEP befasst sich nur mit CPython-Implementierungsdetails, sie wirkt sich nicht auf die Python-Sprache oder die Standardbibliothek aus.
Probleme
Dies listet verschiedene Probleme mit eingebauten Funktionen und Methoden auf, zusammen mit einem Plan für eine Lösung und (falls zutreffend) Verweise auf Standards-Track-PEPs, die die Details diskutieren.
1. Benennung
Das Wort „eingebaut“ wird in Python überstrapaziert. Bei einem schnellen Überfliegen der Python-Dokumentation bezieht es sich meist auf Dinge aus dem Modul builtins. Mit anderen Worten: Dinge, die im globalen Namensraum ohne Import verfügbar sind. Dies widerspricht der Verwendung des Wortes „eingebaut“ im Sinne von „in C implementiert“.
Lösung: Da die C-Struktur für eingebaute Funktionen und Methoden bereits PyCFunctionObject heißt, verwenden wir den Namen „cfunction“ und „cmethod“ anstelle von „eingebaute Funktion“ und „eingebaute Methode“.
2. Nicht erweiterbar
Die verschiedenen beteiligten Klassen (wie builtin_function_or_method) können nicht abgeleitet werden
>>> from types import BuiltinFunctionType
>>> class X(BuiltinFunctionType):
... pass
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: type 'builtin_function_or_method' is not an acceptable base type
Dies ist ein Problem, da es unmöglich macht, Funktionen wie die Unterstützung für Introspektion zu diesen Klassen hinzuzufügen.
Wenn man eine Funktion in C mit zusätzlicher Funktionalität implementieren möchte, muss eine völlig neue Klasse von Grund auf neu implementiert werden. Das Problem dabei ist, dass die bestehenden Klassen wie builtin_function_or_method im Python-Interpreter speziell behandelt werden, um ein schnelleres Aufrufen zu ermöglichen (z. B. durch Verwendung von METH_FASTCALL). Es ist derzeit unmöglich, eine benutzerdefinierte Klasse mit denselben Optimierungen zu haben.
Lösung: die bestehenden Optimierungen für beliebige Klassen verfügbar machen. Dies geschieht durch Hinzufügen eines neuen Feldes PyTypeObject namens tp_ccalloffset (oder können wir tp_print dafür wiederverwenden?), das den Offset eines PyCCallDef-Zeigers angibt. Dies ist eine neue Struktur, die alle für den Aufruf einer cfunction benötigten Informationen enthält, und sie würde anstelle von PyMethodDef verwendet werden. Dies implementiert das neue „C-Aufruf“-Protokoll.
Für die Erstellung von cfunctions und cmethods werden weiterhin PyMethodDef-Arrays verwendet (z. B. in tp_methods), aber das wird der *einzige* verbleibende Zweck der PyMethodDef-Struktur sein.
Zusätzlich können wir auch einige Funktionsklassen ableitbar machen. Dies scheint jedoch weniger wichtig zu sein, sobald wir tp_ccalloffset haben.
Referenz: PEP 580
3. cfunctions werden nicht zu Methoden
Eine cfunction wie repr implementiert __get__ nicht, um sich als Methode zu binden
>>> class X:
... meth = repr
>>> x = X()
>>> x.meth()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: repr() takes exactly one argument (0 given)
In diesem Beispiel hätte man erwartet, dass x.meth() repr(x) zurückgibt, indem die normalen Regeln für Methoden angewendet werden.
Dies ist überraschend und ein unnötiger Unterschied zwischen cfunctions und Python-Funktionen. Für die Standard-eingebauten Funktionen ist dies kein wirkliches Problem, da diese nicht als Methoden verwendet werden sollen. Aber es wird zu einem Problem, wenn man eine neue cfunction mit dem Ziel implementieren möchte, als Methode verwendet zu werden.
Auch hier könnte eine Lösung darin bestehen, eine neue Klasse zu erstellen, die sich wie cfunctions verhält, aber sich als Methoden bindet. Das würde jedoch einige bestehende Optimierungen für Methoden verlieren, wie z. B. die Opcodes LOAD_METHOD/CALL_METHOD.
Lösung: dieselbe wie beim vorherigen Problem. Sie zeigt nur, dass die Handhabung von self und __get__ Teil des neuen C-Aufruf-Protokolls sein sollte.
Aus Gründen der Abwärtskompatibilität würden wir das bestehende Nicht-Bindungsverhalten von cfunctions beibehalten. Wir würden es nur in benutzerdefinierten Klassen zulassen.
Referenz: PEP 580
4. Semantik von inspect.isfunction
Derzeit gibt inspect.isfunction nur für Instanzen von types.FunctionType True zurück. Das heißt, echte Python-Funktionen.
Ein häufiger Anwendungsfall für inspect.isfunction ist die Überprüfung der Introspektion: Sie garantiert beispielsweise, dass inspect.getfile() funktioniert. Idealerweise sollte es auch für andere Klassen möglich sein, als Funktionen behandelt zu werden.
Lösung: Einführung einer neuen abstrakten Basisklasse InspectFunction und Verwendung dieser zur Implementierung von inspect.isfunction. Alternativ Duck-Typing für inspect.isfunction verwenden (wie in [2] vorgeschlagen)
def isfunction(obj):
return hasattr(type(obj), "__code__")
5. C-Funktionen sollten Zugriff auf das Funktions-Objekt haben
Die zugrunde liegende C-Funktion einer cfunction nimmt derzeit ein self-Argument (für gebundene Methoden) und dann möglicherweise eine Anzahl von Argumenten entgegen. Es gibt keine Möglichkeit für die C-Funktion, tatsächlich auf das Python-cfunction-Objekt (das self in __call__ oder tp_call) zuzugreifen. Dies würde beispielsweise die Implementierung des C-Aufruf-Protokolls für Python-Funktionen (types.FunctionType) ermöglichen: Die C-Funktion, die das Aufrufen von Python-Funktionen implementiert, benötigt Zugriff auf das Attribut __code__ der Funktion.
Dies ist auch für PEP 573 erforderlich, bei der alle cfunctions Zugriff auf ihr „Elternteil“ benötigen (das Modul für Funktionen eines Moduls oder die definierende Klasse für Methoden).
Lösung: Hinzufügen eines neuen PyMethodDef-Flags, um anzugeben, dass die C-Funktion ein zusätzliches Argument (als erstes Argument) entgegennimmt, nämlich das Funktions-Objekt.
6. METH_FASTCALL ist privat und undokumentiert
Der METH_FASTCALL-Mechanismus ermöglicht das Aufrufen von cfunctions und cmethods über ein C-Array von Python-Objekten anstelle eines tuple. Dies wurde in Python 3.6 nur für positionsgebundene Argumente eingeführt und in Python 3.7 um Unterstützung für Schlüsselwortargumente erweitert.
Da es jedoch undokumentiert ist, darf es vermutlich nur von CPython selbst verwendet werden.
Lösung: Da dies eine wichtige Optimierung ist, sollte jeder ermutigt werden, sie zu verwenden. Da die Implementierung von METH_FASTCALL nun stabil ist, dokumentieren Sie sie!
Als Teil des C-Aufruf-Protokolls sollten wir auch eine C-API-Funktion hinzufügen
PyObject *PyCCall_FastCall(PyObject *func, PyObject *const *args, Py_ssize_t nargs, PyObject *keywords)
Referenz: PEP 580
7. Zulassen von nativen C-Argumenten
Eine cfunction nimmt immer ihre Argumente als Python-Objekte entgegen (sagen wir, ein Array von PyObject-Zeigern). In Fällen, in denen die cfunction tatsächlich eine native C-Funktion umschließt (z. B. von ctypes oder einem Compiler wie Cython), ist dies ineffizient: Aufrufe von C-Code zu C-Code müssen Python-Objekte verwenden, um Argumente zu übergeben.
Analog zum Pufferprotokoll, das den Zugriff auf C-Daten ermöglicht, sollten wir auch den Zugriff auf das zugrunde liegende C-Aufruf-Objekt ermöglichen.
Lösung: Beim Umschließen einer C-Funktion mit nativen Argumenten (z. B. eines C-long) innerhalb einer cfunction sollten wir auch einen Funktionszeiger auf die zugrunde liegende C-Funktion zusammen mit ihrer C-Signatur speichern.
Argument Clinic könnte dies automatisch tun, indem es einen Zeiger auf die „impl“-Funktion speichert.
8. Komplexität
Es gibt eine riesige Anzahl von Klassen, die an der Implementierung aller Varianten von Methoden beteiligt sind. Das ist an sich kein Problem, aber ein sich verstärkendes Problem.
Für gewöhnliche Python-Klassen gibt die untenstehende Tabelle die Klassen für verschiedene Arten von Methoden an. Die Spalten beziehen sich auf die Klasse im Klassen-__dict__, die Klasse für ungebundene Methoden (gebunden an die Klasse) und die Klasse für gebundene Methoden (gebunden an die Instanz)
| Art | __dict__ | ungebunden | gebunden |
|---|---|---|---|
| Normale Methode | function |
function |
method |
| Statische Methode | staticmethod |
function |
function |
| Klassenmethode | classmethod |
method |
method |
| Slot-Methode | function |
function |
method |
Dies ist die analoge Tabelle für Erweiterungstypen (C-Klassen)
| Art | __dict__ | ungebunden | gebunden |
|---|---|---|---|
| Normale Methode | method_descriptor |
method_descriptor |
builtin_function_or_method |
| Statische Methode | staticmethod |
builtin_function_or_method |
builtin_function_or_method |
| Klassenmethode | classmethod_descriptor |
builtin_function_or_method |
builtin_function_or_method |
| Slot-Methode | wrapper_descriptor |
wrapper_descriptor |
method-wrapper |
Es sind viele Klassen beteiligt, und diese beiden Tabellen sehen sehr unterschiedlich aus. Es gibt keinen guten Grund, warum Python-Methoden grundlegend anders behandelt werden sollten als C-Methoden. Auch die Funktionen sind leicht unterschiedlich: z. B. unterstützt method __func__, aber builtin_function_or_method nicht.
Da CPython Optimierungen für Aufrufe der meisten dieser Objekte hat, kann der Code für die Handhabung dieser auch komplex werden. Ein gutes Beispiel dafür ist die Funktion call_function in Python/ceval.c.
Lösung: Alle diese Klassen sollten das C-Aufruf-Protokoll implementieren. Dann kann die Komplexität im Code größtenteils durch Überprüfung des C-Aufruf-Protokolls (tp_ccalloffset != 0) behoben werden, anstatt Typüberprüfungen durchzuführen.
Darüber hinaus sollte untersucht werden, ob einige dieser Klassen zusammengeführt werden können und ob method auch für gebundene Methoden von Erweiterungstypen wiederverwendet werden kann (siehe PEP 576 für Letzteres, unter Berücksichtigung, dass dies einige geringfügige Kompatibilitätsprobleme aufweisen kann). Dies ist kein Selbstzweck, sondern nur etwas, das bei der Arbeit an diesen Klassen zu beachten ist.
9. PyMethodDef ist zu begrenzt
Die typische Methode zur Erstellung einer cfunction oder cmethod in einem Erweiterungsmodul ist die Verwendung einer PyMethodDef. Diese werden dann in einem Array PyModuleDef.m_methods (für cfunctions) oder PyTypeObject.tp_methods (für cmethods) gespeichert. Aufgrund der stabilen ABI (PEP 384) können wir jedoch die PyMethodDef-Struktur nicht ändern.
Das bedeutet, dass wir keine neuen Felder zum Erstellen von cfunctions/cmethods auf diese Weise hinzufügen können. Dies ist wahrscheinlich der Grund für den Hack, dass __doc__ und __text_signature__ in demselben C-String gespeichert werden (wobei die Deskriptoren __doc__ und __text_signature__ den relevanten Teil extrahieren).
Lösung: Hören Sie auf anzunehmen, dass ein einzelner PyMethodDef-Eintrag ausreicht, um eine cfunction/cmethod zu beschreiben. Stattdessen könnten wir ein Flag hinzufügen, das bedeutet, dass eines der PyMethodDef-Felder stattdessen ein Zeiger auf eine zusätzliche Struktur ist. Oder wir könnten ein Flag hinzufügen, um zwei oder mehr aufeinanderfolgende PyMethodDef-Einträge im Array zu verwenden, um mehr Daten zu speichern. Dann würde das PyMethodDef-Array nur zum Erstellen von cfunctions/cmethods verwendet, aber nicht mehr danach.
10. Slot-Wrapper haben keine benutzerdefinierte Dokumentation
Derzeit haben Slot-Wrapper wie __init__ oder __lt__ nur sehr generische Dokumentationen, die überhaupt nicht klassenspezifisch sind
>>> list.__init__.__doc__
'Initialize self. See help(type(self)) for accurate signature.'
>>> list.__lt__.__doc__
'Return self<value.'
Dasselbe geschieht für die Signatur
>>> list.__init__.__text_signature__
'($self, /, *args, **kwargs)'
Wie Sie sehen können, unterstützen Slot-Wrapper __doc__ und __text_signature__. Das Problem ist, dass diese in struct wrapperbase gespeichert werden, was für alle Wrapper eines bestimmten Slots üblich ist (z. B. derselbe wrapperbase wird für str.__eq__ und int.__eq__ verwendet).
Lösung: Überdenken Sie die Slot-Wrapper-Klasse, um Dokumentationen (und Textsignaturen) für jede Instanz separat zuzulassen.
Dies lässt die Frage offen, wie Erweiterungsmodule die Dokumentation angeben sollen. Die Einträge in PyTypeObject wie tp_init sind nur Funktionszeiger, damit können wir nichts anfangen. Eine Lösung wäre, Einträge zum Array tp_methods hinzuzufügen, nur um Docstrings hinzuzufügen. Ein solcher Eintrag könnte so aussehen
{"__init__", NULL, METH_SLOTDOC, "pointer to __init__ doc goes here"}
11. Statische Methoden und Klassenmethoden sollten aufrufbar sein
Instanzen von staticmethod und classmethod sollten aufrufbar sein. Zugegebenermaßen gibt es dafür keinen starken Anwendungsfall, aber er wurde gelegentlich angefordert (siehe z. B. [1]).
Das Aufrufbar-Machen von statischen/Klassenmethoden würde die Konsistenz erhöhen. Erstens fügen Funktionsdekorektoren typischerweise Funktionalität hinzu oder modifizieren eine Funktion, aber das Ergebnis bleibt aufrufbar. Dies ist bei @staticmethod und @classmethod nicht der Fall.
Zweitens sind Klassenmethoden von Erweiterungstypen bereits aufrufbar
>>> fromhex = float.__dict__["fromhex"]
>>> type(fromhex)
<class 'classmethod_descriptor'>
>>> fromhex(float, "0xff")
255.0
Drittens kann man function, staticmethod und classmethod als verschiedene Arten von ungebundenen Methoden betrachten: Sie werden alle zu method, wenn sie gebunden werden, aber die Implementierung von __get__ ist leicht unterschiedlich. Aus dieser Sicht erscheint es seltsam, dass function aufrufbar ist, die anderen aber nicht.
Lösung: Beim Ändern der Implementierung von staticmethod und classmethod sollten wir in Erwägung ziehen, Instanzen aufrufbar zu machen. Selbst wenn dies kein Selbstzweck ist, kann es aufgrund der Implementierung natürlich geschehen.
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0579.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT