Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 575 – Vereinheitlichung von Funktions-/Methodenklassen

Autor:
Jeroen Demeyer <J.Demeyer at UGent.be>
Status:
Zurückgezogen
Typ:
Standards Track
Erstellt:
27. Mrz. 2018
Python-Version:
3.8
Post-History:
31. Mrz. 2018, 12. Apr. 2018, 27. Apr. 2018, 05. Mai 2018

Inhaltsverzeichnis

Rücknahmehinweis

Siehe PEP 580 für eine bessere Lösung zur Ermöglichung schneller Aufrufe benutzerdefinierter Klassen.

Siehe PEP 579 für eine breitere Diskussion einiger anderer Probleme aus diesem PEP.

Zusammenfassung

Neuordnung der Klassenhierarchie für Funktionen und Methoden mit dem Ziel, die Unterschiede zwischen Built-in-Funktionen (in C implementiert) und Python-Funktionen zu reduzieren. Hauptsächlich sollen Built-in-Funktionen wie Python-Funktionen verhalten, ohne Leistungseinbußen.

Eine neue Basisklasse base_function wird eingeführt und die verschiedenen Funktionsklassen sowie method (umbenannt in bound_method) erben davon.

Wir erlauben auch die Unterklassifizierung der Python-function-Klasse.

Motivation

Derzeit hat CPython zwei verschiedene Funktionsklassen: Die erste sind Python-Funktionen, die man erhält, wenn man eine Funktion mit def oder lambda definiert. Die zweite sind Built-in-Funktionen wie len, isinstance oder numpy.dot. Diese sind in C implementiert.

Diese beiden Klassen sind vollständig unabhängig voneinander implementiert und haben unterschiedliche Funktionalitäten. Insbesondere ist es derzeit nicht möglich, eine Funktion effizient in C zu implementieren (nur Built-in-Funktionen können das), während gleichzeitig Introspektion wie inspect.signature oder inspect.getsourcefile ermöglicht wird (nur Python-Funktionen können das). Dies ist ein Problem für Projekte wie Cython [1], die genau das tun möchten.

In Cython wurde dies durch die Erfindung einer neuen Funktionsklasse namens cyfunction umgangen. Leider verursacht eine neue Funktionsklasse Probleme: Das inspect-Modul erkennt solche Funktionen nicht als Funktionen [2] und die Leistung ist schlechter (CPython hat spezifische Optimierungen für den Aufruf von Built-in-Funktionen).

Eine zweite Motivation ist generell, Built-in-Funktionen und -Methoden sich wie Python-Funktionen und -Methoden verhalten zu lassen. Zum Beispiel sind Python ungebundene Methoden einfach Funktionen, aber ungebundene Methoden von Erweiterungstypen (z.B. dict.get) sind eine eigene Klasse. Gebundene Methoden von Python-Klassen haben ein __func__-Attribut, gebundene Methoden von Erweiterungstypen nicht.

Drittens ermöglicht dieses PEP eine große Anpassbarkeit von Funktionen. Die function-Klasse wird unterklassifizierbar und benutzerdefinierte Funktionsunterklassen sind auch für in C implementierte Funktionen erlaubt. In letzterem Fall kann dies mit der gleichen Leistung wie echte Built-in-Funktionen erfolgen. Alle Funktionen können auf das Funktions-Objekt zugreifen (das self in __call__), was den Weg für PEP 573 ebnet.

Neue Klassen

Dies ist die neue Klassenhierarchie für Funktionen und Methoden

              object
                 |
                 |
          base_function
         /       |     \
        /        |      \
       /         |   defined_function
      /          |        \
cfunction (*)    |         \
                 |       function
                 |
           bound_method (*)

Die beiden mit (*) gekennzeichneten Klassen erlauben *keine* Unterklassifizierung; die anderen schon.

Es gibt keinen Unterschied zwischen Funktionen und ungebundenen Methoden, während gebundene Methoden Instanzen von bound_method sind.

base_function

Die Klasse base_function wird eine neue Basisklasse für alle Funktionstypen. Sie basiert auf der bestehenden builtin_function_or_method-Klasse, jedoch mit folgenden Unterschieden und neuen Funktionen:

  1. Sie fungiert als Deskriptor, der __get__ implementiert, um eine Funktion in eine Methode umzuwandeln, wenn m_self NULL ist. Wenn m_self nicht NULL ist, ist dies eine No-Op: die bestehende Funktion wird stattdessen zurückgegeben.
  2. Ein neues schreibgeschütztes Attribut __parent__, das in der C-Struktur als m_parent dargestellt wird. Wenn dieses Attribut existiert, repräsentiert es das definierende Objekt. Für Methoden von Erweiterungstypen ist dies die definierende Klasse (__class__ in reinem Python) und für Funktionen eines Moduls ist dies das definierende Modul. Im Allgemeinen kann es jedes Python-Objekt sein. Wenn __parent__ eine Klasse ist, trägt es eine spezielle Semantik: in diesem Fall muss die Funktion mit self als Instanz dieser Klasse aufgerufen werden. Schließlich verwenden __qualname__ und __reduce__ __parent__ als Namensraum (statt __self__ zuvor).
  3. Ein neues Attribut __objclass__, das gleich __parent__ ist, wenn __parent__ eine Klasse ist. Andernfalls löst der Zugriff auf __objclass__ einen AttributeError aus. Dies ist zur Rückwärtskompatibilität mit method_descriptor gedacht.
  4. Das Feld ml_doc und die Attribute __doc__ und __text_signature__ (siehe Argument Clinic) werden nicht unterstützt.
  5. Ein neues Flag METH_PASS_FUNCTION für ml_flags. Wenn dieses Flag gesetzt ist, wird die in ml_meth gespeicherte C-Funktion mit einem zusätzlichen ersten Argument, das dem Funktions-Objekt entspricht, aufgerufen.
  6. Ein neues Flag METH_BINDING für ml_flags, das nur für Funktionen eines Moduls (nicht für Methoden einer Klasse) gilt. Wenn dieses Flag gesetzt ist, wird m_self auf NULL statt auf das Modul gesetzt. Dies ermöglicht es der Funktion, sich wie eine Python-Funktion zu verhalten, da es __get__ aktiviert.
  7. Ein neues Flag METH_CALL_UNBOUND, um das Self-Slicing zu deaktivieren.
  8. Ein neues Flag METH_PYTHON für ml_flags. Dieses Flag gibt an, dass diese Funktion als Python-Funktion behandelt werden sollte. Idealerweise sollte die Verwendung dieses Flags vermieden werden, da es gegen die Duck-Typing-Philosophie verstößt. Sie ist jedoch noch an einigen Stellen erforderlich, z.B. für Profiling.

Das Ziel von base_function ist es, alle unterschiedlichen Arten des Aufrufs von Funktionen und Methoden in nur einer Struktur zu unterstützen. Zum Beispiel wird das neue Flag METH_PASS_FUNCTION von der Implementierung von Methoden verwendet.

Es ist nicht möglich, Instanzen von base_function direkt zu erstellen (tp_new ist NULL). Es ist jedoch für C-Code legal, Instanzen manuell zu erstellen.

Dies sind die relevanten C-Strukturen

PyTypeObject PyBaseFunction_Type;

typedef struct {
    PyObject_HEAD
    PyCFunctionDef *m_ml;     /* Description of the C function to call */
    PyObject *m_self;         /* __self__: anything, can be NULL; readonly */
    PyObject *m_module;       /* __module__: anything (typically str) */
    PyObject *m_parent;       /* __parent__: anything, can be NULL; readonly */
    PyObject *m_weakreflist;  /* List of weak references */
} PyBaseFunctionObject;

typedef struct {
    const char *ml_name;   /* The name of the built-in function/method */
    PyCFunction ml_meth;   /* The C function that implements it */
    int ml_flags;          /* Combination of METH_xxx flags, which mostly
                              describe the args expected by the C func */
} PyCFunctionDef;

Unterklassen können PyCFunctionDef um zusätzliche Felder erweitern.

Das Python-Attribut __self__ gibt m_self zurück, außer wenn METH_STATIC gesetzt ist. In diesem Fall oder wenn m_self NULL ist, gibt es kein __self__-Attribut. Aus diesem Grund schreiben wir in diesem PEP entweder m_self oder __self__ mit leicht unterschiedlichen Bedeutungen.

cfunction

Dies ist die neue Version der alten Klasse builtin_function_or_method. Der Name cfunction wurde gewählt, um Verwechslungen mit "built-in" im Sinne von "etwas im builtins-Modul" zu vermeiden. Er passt auch besser zur C API, die das Präfix PyCFunction verwendet.

Die Klasse cfunction ist eine Kopie von base_function mit folgenden Unterschieden:

  1. m_ml zeigt auf eine PyMethodDef-Struktur, die PyCFunctionDef um ein zusätzliches Feld ml_doc erweitert, um __doc__ und __text_signature__ als schreibgeschützte Attribute zu implementieren.
    typedef struct {
        const char *ml_name;
        PyCFunction ml_meth;
        int ml_flags;
        const char *ml_doc;
    } PyMethodDef;
    

    Beachten Sie, dass PyMethodDef Teil der Python Stable ABI ist und praktisch von allen Erweiterungsmodulen verwendet wird. Daher können wir diese Struktur absolut nicht ändern.

  2. Argument Clinic wird unterstützt.
  3. __self__ existiert immer. In Fällen, in denen base_function.__self__ einen AttributeError auslösen würde, wird stattdessen None zurückgegeben.

Das Typobjekt ist PyTypeObject PyCFunction_Type und wir definieren PyCFunctionObject als Alias für PyBaseFunctionObject (außer für den Typ von m_ml).

defined_function

Die Klasse defined_function ist eine abstrakte Basisklasse, die anzeigen soll, dass die Funktion Introspektionsunterstützung hat. Instanzen von defined_function müssen alle Attribute unterstützen, die Python-Funktionen haben, nämlich __code__, __globals__, __doc__, __defaults__, __kwdefaults__, __closure__ und __annotations__. Es gibt auch ein __dict__, um vom Benutzer hinzugefügte Attribute zu unterstützen.

Keines davon muss sinnvoll sein. Insbesondere muss __code__ kein funktionierendes Code-Objekt sein, möglicherweise sind nur wenige Felder ausgefüllt. Dieses PEP schreibt nicht vor, wie die verschiedenen Attribute implementiert werden. Sie können einfache Strukturmitglieder oder kompliziertere Deskriptoren sein. Nur schreibgeschützte Unterstützung ist erforderlich, keines der Attribute muss schreibbar sein.

Die Klasse defined_function ist hauptsächlich für automatisch generierten C-Code gedacht, der beispielsweise von Cython [1] produziert wird. Es gibt keine API, um Instanzen davon zu erstellen.

Die C-Struktur ist die folgende

PyTypeObject PyDefinedFunction_Type;

typedef struct {
    PyBaseFunctionObject base;
    PyObject *func_dict;        /* __dict__: dict or NULL */
} PyDefinedFunctionObject;

TODO: vielleicht einen besseren Namen für defined_function finden. Andere Vorschläge: inspect_function (alles, was inspect.isfunction erfüllt), builtout_function (eine Funktion, die besser "built out" ist; Wortspiel mit "builtin"), generic_function (ursprünglicher Vorschlag, aber Konflikt mit functools.singledispatch generischen Funktionen), user_function (vom Benutzer definiert im Gegensatz zu CPython).

function

Dies ist die Klasse für in Python implementierte Funktionen. Im Gegensatz zu den anderen Funktionstypen können Instanzen von function aus Python-Code erstellt werden. Dies wird nicht geändert, daher beschreiben wir die Details nicht in diesem PEP.

Das Layout der C-Struktur ist die folgende

PyTypeObject PyFunction_Type;

typedef struct {
    PyBaseFunctionObject base;
    PyObject *func_dict;        /* __dict__: dict or NULL */
    PyObject *func_code;        /* __code__: code */
    PyObject *func_globals;     /* __globals__: dict; readonly */
    PyObject *func_name;        /* __name__: string */
    PyObject *func_qualname;    /* __qualname__: string */
    PyObject *func_doc;         /* __doc__: can be anything or NULL */
    PyObject *func_defaults;    /* __defaults__: tuple or NULL */
    PyObject *func_kwdefaults;  /* __kwdefaults__: dict or NULL */
    PyObject *func_closure;     /* __closure__: tuple of cell objects or NULL; readonly */
    PyObject *func_annotations; /* __annotations__: dict or NULL */
    PyCFunctionDef _ml;         /* Storage for base.m_ml */
} PyFunctionObject;

Der Deskriptor __name__ gibt func_name zurück. Beim Setzen von __name__ wird auch base.m_ml->ml_name mit dem UTF-8-kodierten Namen aktualisiert.

Das Feld _ml reserviert Platz, der von base.m_ml verwendet wird.

Eine base_function-Instanz muss das Flag METH_PYTHON gesetzt haben, wenn und nur wenn sie eine Instanz von function ist.

Beim Konstruieren einer Instanz von function aus code und globals wird eine Instanz mit base.m_ml = &_ml und base.m_self = NULL erstellt.

Um die Unterklassifizierung zu erleichtern, fügen wir auch einen Kopierkonstruktor hinzu: Wenn f eine Instanz von function ist, dann kopiert types.FunctionType(f) f. Dies ermöglicht es bequem, einen benutzerdefinierten Funktionstyp als Dekorator zu verwenden.

>>> from types import FunctionType
>>> class CustomFunction(FunctionType):
...     pass
>>> @CustomFunction
... def f(x):
...     return x
>>> type(f)
<class '__main__.CustomFunction'>

Dies entfernt auch viele Anwendungsfälle von functools.wraps: Wrapper können durch Unterklassen von function ersetzt werden.

bound_method

Die Klasse bound_method wird für alle gebundenen Methoden verwendet, unabhängig von der Klasse der zugrunde liegenden Funktion. Sie fügt ein neues Attribut zu base_function hinzu: __func__ zeigt auf diese Funktion.

bound_method ersetzt die alte Klasse method, die nur für als Methode gebundene Python-Funktionen verwendet wurde.

Es gibt eine Komplikation, da wir die Erstellung einer Methode aus einem beliebigen aufrufbaren Objekt zulassen wollen. Dies kann eine bereits gebundene Methode sein oder einfach keine Instanz von base_function. Daher gibt es praktisch zwei Arten von Methoden:

  • Für beliebige Aufrufbare verwenden wir eine einzige feste PyCFunctionDef-Struktur mit dem gesetzten Flag METH_PASS_FUNCTION.
  • Für Methoden, die Instanzen von base_function binden (genauer gesagt, die das Flag Py_TPFLAGS_BASEFUNCTION gesetzt haben) und die Self-Slicing haben, verwenden wir stattdessen die PyCFunctionDef der ursprünglichen Funktion. Auf diese Weise verlieren wir keine Leistung beim Aufruf gebundener Methoden. In diesem Fall wird das __func__-Attribut nur zur Implementierung verschiedener Attribute verwendet, aber nicht zum Aufrufen der Methode.

Beim Erstellen einer neuen Methode aus einer base_function prüfen wir, ob das self-Objekt eine Instanz von __objclass__ ist (wenn eine Klasse als Elternteil angegeben wurde) und lösen andernfalls einen TypeError aus.

Die C-Struktur ist

PyTypeObject PyMethod_Type;

typedef struct {
    PyBaseFunctionObject base;
    PyObject *im_func;  /* __func__: function implementing the method; readonly */
} PyMethodObject;

Aufrufen von base_function-Instanzen

Wir geben die Implementierung von __call__ für Instanzen von base_function an.

Überprüfung von __objclass__

Zunächst erfolgt eine Typenprüfung, ob das __parent__ der Funktion eine Klasse ist (zur Erinnerung: __objclass__ wird dann zu einem Alias von __parent__): Wenn m_self NULL ist (dies ist der Fall bei ungebundenen Methoden von Erweiterungstypen), dann muss die Funktion mit mindestens einem positionsbezogenen Argument aufgerufen werden und das erste (typischerweise self genannte) muss eine Instanz von __objclass__ sein. Andernfalls wird ein TypeError ausgelöst.

Beachten Sie, dass gebundene Methoden m_self != NULL haben, daher wird __objclass__ nicht überprüft. Stattdessen erfolgt die Prüfung von __objclass__ beim Erstellen der Methode.

Flags

Zur Vereinfachung definieren wir eine neue Konstante: METH_CALLFLAGS kombiniert alle Flags aus PyCFunctionDef.ml_flags, die die Signatur der aufzurufenden C-Funktion angeben. Sie ist gleich

METH_VARARGS | METH_FASTCALL | METH_NOARGS | METH_O | METH_KEYWORDS | METH_PASS_FUNCTION

Genau eines der ersten vier Flags muss gesetzt sein, und nur METH_VARARGS und METH_FASTCALL dürfen mit METH_KEYWORDS kombiniert werden. Verstoß gegen diese Regeln ist undefiniertes Verhalten.

Es gibt ein neues Flag, das den Aufruf von Funktionen beeinflusst, nämlich METH_PASS_FUNCTION und METH_CALL_UNBOUND. Einige Flags sind bereits in [5] dokumentiert. Wir erklären die anderen unten.

Self-Slicing

Wenn die Funktion m_self == NULL hat und das Flag METH_CALL_UNBOUND nicht gesetzt ist, wird das erste positionsbezogene Argument (falls vorhanden) aus *args entfernt und stattdessen als erstes Argument an die C-Funktion übergeben. Effektiv wird das erste positionsbezogene Argument als __self__ behandelt. Dies soll ungebundene Methoden unterstützen, sodass die C-Funktion den Unterschied zwischen Aufrufen gebundener und ungebundener Methoden nicht sieht. Dies hat keine Auswirkungen auf Keyword-Argumente.

Dieser Prozess wird als Self-Slicing bezeichnet und eine Funktion hat Self-Slicing, wenn m_self == NULL und METH_CALL_UNBOUND nicht gesetzt ist.

Beachten Sie, dass eine METH_NOARGS-Funktion mit Self-Slicing effektiv ein Argument hat, nämlich self. Analog hat eine METH_O-Funktion mit Self-Slicing zwei Argumente.

METH_PASS_FUNCTION

Wenn dieses Flag gesetzt ist, wird die C-Funktion mit einem zusätzlichen ersten Argument aufgerufen, nämlich der Funktion selbst (der base_function-Instanz). Als Sonderfall, wenn die Funktion eine bound_method ist, wird die zugrunde liegende Funktion der Methode übergeben (aber nicht rekursiv: Wenn eine bound_method eine bound_method umhüllt, wird __func__ nur einmal angewendet).

Zum Beispiel hat eine normale METH_VARARGS-Funktion die Signatur (PyObject *self, PyObject *args). Mit METH_VARARGS | METH_PASS_FUNCTION wird daraus (PyObject *func, PyObject *self, PyObject *args).

METH_FASTCALL

Dies ist ein existierendes, aber undokumentiertes Flag. Wir schlagen vor, es offiziell zu unterstützen und zu dokumentieren.

Wenn das Flag METH_FASTCALL ohne METH_KEYWORDS gesetzt ist, dann ist das Feld ml_meth vom Typ PyCFunctionFast, die die Argumente (PyObject *self, PyObject *const *args, Py_ssize_t nargs) nimmt. Eine solche Funktion nimmt nur positionsbezogene Argumente und diese werden als einfacher C-Array args der Länge nargs übergeben.

Wenn die Flags METH_FASTCALL | METH_KEYWORDS gesetzt sind, dann ist das Feld ml_meth vom Typ PyCFunctionFastKeywords, die die Argumente (PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) nimmt. Die positionsbezogenen Argumente werden als C-Array args der Länge nargs übergeben. Die *Werte* der Keyword-Argumente folgen in diesem Array, beginnend an Position nargs. Die *Schlüssel* (Namen) der Keyword-Argumente werden als tuple in kwnames übergeben. Als Beispiel, nehmen wir an, dass 3 positionsbezogene und 2 Keyword-Argumente gegeben sind. Dann ist args ein Array der Länge 3 + 2 = 5, nargs gleich 3 und kwnames ist ein 2-Tupel.

Automatische Erstellung von Built-in-Funktionen

Python generiert automatisch Instanzen von cfunction für Erweiterungstypen (über das Feld PyTypeObject.tp_methods) und Module (über das Feld PyModuleDef.m_methods). Die Arrays PyTypeObject.tp_methods und PyModuleDef.m_methods müssen Arrays von PyMethodDef-Strukturen sein.

Ungebundene Methoden von Erweiterungstypen

Der Typ von ungebundenen Methoden ändert sich von method_descriptor zu cfunction. Das Objekt, das als ungebundene Methode erscheint, ist dasselbe Objekt, das in der __dict__ der Klasse erscheint. Python setzt automatisch das Attribut __parent__ auf die definierende Klasse.

Built-in-Funktionen eines Moduls

Für den Fall von Funktionen eines Moduls wird __parent__ auf das Modul gesetzt. Es sei denn, das Flag METH_BINDING ist angegeben, dann wird auch __self__ auf das Modul gesetzt (zur Rückwärtskompatibilität).

Eine wichtige Konsequenz ist, dass solche Funktionen standardmäßig keine Methoden werden, wenn sie als Attribut verwendet werden (base_function.__get__ tut dies nur, wenn m_self NULL war). Man könnte dies als Fehler betrachten, aber dies geschah aus Gründen der Abwärtskompatibilität: In einem ursprünglichen Beitrag auf python-ideas [6] bestand der Konsens darin, dieses Fehlverhalten von eingebauten Funktionen beizubehalten.

Um dies jedoch für bestimmte oder neu implementierte eingebaute Funktionen zuzulassen, verhindert das METH_BINDING Flag das Setzen von __self__.

Weitere Änderungen

Neues Typ-Flag

Ein neues PyTypeObject Flag (für tp_flags) wird hinzugefügt: Py_TPFLAGS_BASEFUNCTION, um anzuzeigen, dass Instanzen dieses Typs Funktionen sind, die wie eine base_function als Methode aufgerufen und gebunden werden können.

Dies unterscheidet sich von Flags wie Py_TPFLAGS_LIST_SUBCLASS, da es mehr als nur eine Unterklasse anzeigt: Es zeigt auch eine Standardimplementierung von __call__ und __get__ an. Insbesondere müssen solche Unterklassen von base_function der Implementierung im Abschnitt Aufruf von base_function-Instanzen folgen.

Dieses Flag wird automatisch für Erweiterungstypen gesetzt, die die tp_call und tp_descr_get Implementierung von base_function erben. Erweiterungstypen können es explizit angeben, wenn sie __call__ oder __get__ auf kompatible Weise überschreiben. Das Flag Py_TPFLAGS_BASEFUNCTION darf niemals für einen Heap-Typ gesetzt werden, da dies nicht sicher wäre (Heap-Typen können dynamisch geändert werden).

C API-Funktionen

Wir listen einige relevante Python/C API Makros und Funktionen auf. Einige davon sind bestehende (möglicherweise geänderte) Funktionen, einige sind neu

  • int PyBaseFunction_CheckFast(PyObject *op): Gibt true zurück, wenn op eine Instanz einer Klasse mit gesetztem Py_TPFLAGS_BASEFUNCTION Flag ist. Dies ist die Funktion, die Sie verwenden müssen, um zu bestimmen, ob es sinnvoll ist, auf die base_function Interna zuzugreifen.
  • int PyBaseFunction_Check(PyObject *op): Gibt true zurück, wenn op eine Instanz von base_function ist.
  • PyObject *PyBaseFunction_New(PyTypeObject *cls, PyCFunctionDef *ml, PyObject *self, PyObject *module, PyObject *parent): Erstellt eine neue Instanz von cls (die eine Unterklasse von base_function sein muss) aus den gegebenen Daten.
  • int PyCFunction_Check(PyObject *op): Gibt true zurück, wenn op eine Instanz von cfunction ist.
  • int PyCFunction_NewEx(PyMethodDef* ml, PyObject *self, PyObject* module): Erstellt eine neue Instanz von cfunction. Als Sonderfall, wenn self NULL ist, wird stattdessen self = Py_None gesetzt (aus Gründen der Abwärtskompatibilität). Wenn self ein Modul ist, dann wird __parent__ auf self gesetzt. Andernfalls ist __parent__ NULL.
  • Für viele bestehende PyCFunction_... und PyMethod_ Funktionen definieren wir eine neue Funktion PyBaseFunction_..., die auf base_function Instanzen operiert. Die alten Funktionen werden als Aliase für die neuen Funktionen beibehalten.
  • int PyFunction_Check(PyObject *op): Gibt true zurück, wenn op eine Instanz von base_function mit gesetztem METH_PYTHON Flag ist (dies ist äquivalent zur Überprüfung, ob op eine Instanz von function ist).
  • int PyFunction_CheckFast(PyObject *op): Äquivalent zu PyFunction_Check(op) && PyBaseFunction_CheckFast(op).
  • int PyFunction_CheckExact(PyObject *op): Gibt true zurück, wenn der Typ von op function ist.
  • PyObject *PyFunction_NewPython(PyTypeObject *cls, PyObject *code, PyObject *globals, PyObject *name, PyObject *qualname): Erstellt eine neue Instanz von cls (die eine Unterklasse von function sein muss) aus den gegebenen Daten.
  • PyObject *PyFunction_New(PyObject *code, PyObject *globals): Erstellt eine neue Instanz von function.
  • PyObject *PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname): Erstellt eine neue Instanz von function.
  • PyObject *PyFunction_Copy(PyTypeObject *cls, PyObject *func): Erstellt eine neue Instanz von cls (die eine Unterklasse von function sein muss) durch Kopieren einer gegebenen function.

Änderungen am types-Modul

Zwei Typen werden hinzugefügt: types.BaseFunctionType entsprechend base_function und types.DefinedFunctionType entsprechend defined_function.

Abgesehen davon werden keine Änderungen am types Modul vorgenommen. Insbesondere bezieht sich types.FunctionType auf function. Die tatsächlichen Typen werden sich jedoch ändern: insbesondere wird types.BuiltinFunctionType nicht mehr dasselbe sein wie types.BuiltinMethodType.

Änderungen am inspect-Modul

Die neue Funktion inspect.isbasefunction prüft auf eine Instanz von base_function.

inspect.isfunction prüft auf eine Instanz von defined_function.

inspect.isbuiltin prüft auf eine Instanz von cfunction.

inspect.isroutine prüft isbasefunction oder ismethoddescriptor.

HINWEIS: bpo-33261 [3] sollte zuerst behoben werden.

Profiling

Derzeit unterstützt sys.setprofile c_call, c_return und c_exception Ereignisse für eingebaute Funktionen. Diese Ereignisse werden beim Aufruf oder bei der Rückkehr von einer eingebauten Funktion generiert. Im Gegensatz dazu werden die Ereignisse call und return von der Funktion selbst generiert. Daher muss für die Ereignisse call und return nichts geändert werden.

Da wir keinen Unterschied mehr zwischen C-Funktionen und Python-Funktionen machen, müssen wir die c_* Ereignisse für Python-Funktionen verhindern. Dies geschieht, indem diese Ereignisse nicht generiert werden, wenn das METH_PYTHON Flag in ml_flags gesetzt ist.

Nicht-CPython-Implementierungen

Der Großteil dieses PEPs ist nur für CPython relevant. Für andere Implementierungen von Python sind die beiden erforderlichen Änderungen die base_function Basisklasse und die Tatsache, dass function unterklassifiziert werden kann. Die Klassen cfunction und defined_function sind nicht erforderlich.

Wir benötigen base_function aus Konsistenzgründen, aber wir legen keine Anforderungen daran: Es ist akzeptabel, wenn dies nur eine Kopie von object ist. Unterstützung für das neue Attribut __parent__ (und __objclass__) ist nicht erforderlich. Wenn keine defined_function Klasse vorhanden ist, sollte types.DefinedFunctionType ein Alias für types.FunctionType sein.

Begründung

Warum nicht einfach bestehende Klassen ändern?

Man könnte versuchen, das Problem zu lösen, indem man die bestehenden Klassen beibehält, ohne eine neue base_function Klasse einzuführen.

Das mag wie eine einfachere Lösung erscheinen, ist es aber nicht: Es würde Introspektionsunterstützung für 3 verschiedene Klassen erfordern: function, builtin_function_or_method und method_descriptor. Für die beiden letzteren Klassen würde "Introspektionsunterstützung" mindestens die Möglichkeit der Unterklassifizierung bedeuten. Aber wir wollen keine Leistung verlieren, daher wünschen wir uns schnelle Unterklassenprüfungen. Dies würde zwei neue Flags in tp_flags erfordern. Und wir möchten, dass Unterklassen __get__ für eingebaute Funktionen erlauben, daher sollten wir auch den LOAD_METHOD Opcode für eingebaute Funktionen implementieren. Allgemeiner gesagt, würde viel Funktionalität dupliziert werden müssen und das Endergebnis wäre ein wesentlich komplexerer Code.

Es ist auch nicht klar, wie die Introspektion von eingebauten Funktionsunterklassen mit __text_signature__ interagieren würde. Zwei unabhängige Arten von inspect.signature Unterstützung auf derselben Klasse zu haben, klingt nach Problemen.

Und dies würde einige der anderen Unterschiede zwischen eingebauten Funktionen und Python-Funktionen, die in der Motivation erwähnt wurden, nicht beheben.

Warum __text_signature__ keine Lösung ist

Eingebaute Funktionen haben ein Attribut __text_signature__, das die Signatur der Funktion als Klartext angibt. Die Standardwerte werden von ast.literal_eval ausgewertet. Aus diesem Grund unterstützt es nur eine kleine Anzahl von Standard-Python-Klassen und keine beliebigen Python-Objekte.

Und selbst wenn __text_signature__ irgendwie beliebige Signaturen zulassen würde, ist das nur ein Teil der Introspektion: Es hilft zum Beispiel nicht bei inspect.getsourcefile.

defined_function im Vergleich zu function

An vielen Stellen muss entschieden werden, ob die alte function Klasse durch defined_function oder die neue function Klasse ersetzt werden soll. Dies geschieht durch Überlegungen zum wahrscheinlichsten Anwendungsfall

  1. types.FunctionType bezieht sich auf function, da dieser Typ zur Konstruktion von Instanzen mit types.FunctionType(...) verwendet werden könnte.
  2. inspect.isfunction() bezieht sich auf defined_function, da dies die Klasse ist, für die Introspektion unterstützt wird.
  3. Die C API Funktionen müssen sich auf function beziehen, da wir nicht spezifizieren, wie die verschiedenen Attribute von defined_function implementiert werden. Wir gehen davon aus, dass dies kein Problem darstellt, da es typischerweise keinen Grund für Introspektion durch C-Erweiterungen gibt.

Umfang dieses PEPs: Welche Klassen sind involviert?

Die Hauptmotivation dieses PEP ist die Korrektur von Funktionsklassen, daher möchten wir unbedingt die bestehenden Klassen builtin_function_or_method und function vereinheitlichen.

Da eingebaute Funktionen und Methoden dieselbe Klasse haben, scheint es natürlich, auch gebundene Methoden einzuschließen. Und da es keine "ungebundenen Methoden" für Python-Funktionen gibt, ist es sinnvoll, ungebundene Methoden für Erweiterungstypen abzuschaffen.

Vorerst werden keine Änderungen an den Klassen staticmethod, classmethod und classmethod_descriptor vorgenommen. Es wäre sicherlich sinnvoll, diese in die base_function Klassenhierarchie aufzunehmen und classmethod und classmethod_descriptor zu vereinheitlichen. Dieses PEP ist jedoch bereits groß genug, und dies wird als mögliche zukünftige Verbesserung belassen.

Slot-Wrapper für Erweiterungstypen wie __init__ oder __eq__ unterscheiden sich erheblich von normalen Methoden. Sie werden auch typischerweise nicht direkt aufgerufen, da man normalerweise foo[i] anstelle von foo.__getitem__(i) schreiben würde. Daher bleiben diese außerhalb des Rahmens dieses PEP.

Python hat auch eine instancemethod Klasse, die ein Relikt aus Python 2 zu sein scheint, wo sie für gebundene und ungebundene Methoden verwendet wurde. Es ist nicht klar, ob es dafür noch einen Anwendungsfall gibt. In jedem Fall gibt es keinen Grund, sich in diesem PEP damit zu befassen.

TODO: Sollte instancemethod als veraltet markiert werden? Es scheint innerhalb von CPython 3.7 überhaupt nicht verwendet zu werden, aber vielleicht verwenden externe Pakete es?

METH_STATIC und METH_CLASS nicht behandelt

Fast nichts in diesem PEP bezieht sich auf die Flags METH_STATIC und METH_CLASS. Diese Flags werden nur von der automatischen Erstellung von eingebauten Funktionen geprüft. Wenn ein staticmethod, classmethod oder classmethod_descriptor gebunden wird (d. h. __get__ aufgerufen wird), wird eine base_function Instanz mit m_self != NULL erstellt. Für eine classmethod ist dies offensichtlich, da m_self die Klasse ist, an die die Methode gebunden wird. Für ein staticmethod kann ein beliebiges Python-Objekt für m_self verwendet werden. Aus Gründen der Abwärtskompatibilität wählen wir m_self = __parent__ für statische Methoden von Erweiterungstypen.

__self__ in base_function

Es mag auf den ersten Blick seltsam erscheinen, den __self__ Slot in base_function anstelle von bound_method hinzuzufügen. Diese Idee haben wir von der bestehenden builtin_function_or_method Klasse übernommen. Sie ermöglicht uns eine einzige allgemeine Implementierung von __call__ und __get__ für die verschiedenen in diesem PEP diskutierten Funktionsklassen.

Es erleichtert auch die Unterstützung bestehender eingebauter Funktionen, die __self__ auf das Modul setzen (z. B. ist sys.exit.__self__ sys).

Zwei Implementierungen von __doc__

base_function unterstützt keine Funktions-Docstrings. Stattdessen haben die Klassen cfunction und function jeweils ihre eigene Art, mit Docstrings umzugehen (und bound_method übernimmt einfach den __doc__ von der umwickelten Funktion).

Für cfunction wird der Docstring (zusammen mit der Textsignatur) als C-String im schreibgeschützten Feld ml_doc einer PyMethodDef gespeichert. Für function wird der Docstring als beschreibbares Python-Objekt gespeichert und muss nicht unbedingt ein String sein. Es erscheint schwierig, diese beiden sehr unterschiedlichen Wege, mit __doc__ umzugehen, zu vereinheitlichen. Aus Gründen der Abwärtskompatibilität behalten wir die bestehenden Implementierungen bei.

Für defined_function verlangen wir, dass __doc__ implementiert wird, aber wir geben nicht vor, wie. Eine Unterklasse kann __doc__ auf die gleiche Weise wie cfunction oder mit einem Strukturmitglied oder auf andere Weise implementieren.

Unterklasse

Wir verbieten die Unterklassifizierung von cfunction und bound_method, um schnelle Typprüfungen für PyCFunction_Check und PyMethod_Check zu ermöglichen.

Wir erlauben die Unterklassifizierung der anderen Klassen, da es keinen Grund gibt, dies zu verbieten. Für Python-Module ist die einzige relevante Klasse, die unterklassifiziert werden kann, function, da die anderen ohnehin nicht instanziiert werden können.

Ersetzen von tp_call: METH_PASS_FUNCTION und METH_CALL_UNBOUND

Die neuen Flags METH_PASS_FUNCTION und METH_CALL_UNBOUND sollen Fälle unterstützen, in denen früher ein benutzerdefinierter tp_call verwendet wurde. Dies reduziert die Anzahl von speziellen Schnellwegen in Python/ceval.c für den Aufruf von Objekten: Anstatt Python-Funktionen, eingebaute Funktionen und Methoden-Deskriptoren getrennt zu behandeln, gäbe es nur eine einzige Prüfung.

Die Signatur von tp_call ist im Wesentlichen die Signatur von PyBaseFunctionObject.m_ml.ml_meth mit den Flags METH_VARARGS | METH_KEYWORDS | METH_PASS_FUNCTION | METH_CALL_UNBOUND (der einzige Unterschied ist ein zusätzliches self Argument). Daher sollte es einfach sein, bestehende tp_call Slots zu ändern, um stattdessen die base_function Implementierung zu verwenden.

Es ist auch sinnvoll, METH_PASS_FUNCTION ohne METH_CALL_UNBOUND in Fällen zu verwenden, in denen die C-Funktion einfach Zugriff auf zusätzliche Metadaten aus der Funktion benötigt, wie z. B. __parent__. Dies ist zum Beispiel erforderlich, um PEP 573 zu unterstützen. Die Konvertierung bestehender Methoden zur Verwendung von METH_PASS_FUNCTION ist trivial: sie erfordert nur das Hinzufügen eines zusätzlichen Arguments zur C-Funktion.

Abwärtskompatibilität

Bei der Konzeption dieses PEPs wurde große Sorgfalt darauf gelegt, die Abwärtskompatibilität nicht zu stark zu brechen. Die meisten potenziell inkompatiblen Änderungen sind Änderungen an CPython-Implementierungsdetails, die in anderen Python-Interpretern ohnehin anders sind. Insbesondere Python-Code, der auf PyPy korrekt läuft, wird mit diesem PEP wahrscheinlich weiterhin funktionieren.

Die Standardklassen und Funktionen wie staticmethod, functools.partial oder operator.methodcaller müssen überhaupt nicht geändert werden.

Änderungen an types und inspect

Die vorgeschlagenen Änderungen an types und inspect zielen darauf ab, Verhaltensänderungen zu minimieren. Es ist jedoch unvermeidlich, dass sich einige Dinge ändern und dies dazu führen kann, dass Code, der types oder inspect verwendet, fehlschlägt. In der Python-Standardbibliothek sind beispielsweise Änderungen im doctest Modul aufgrund dessen erforderlich.

Auch Werkzeuge, die verschiedene Arten von Funktionen als Eingabe nehmen, müssen sich mit der neuen Funktionshierarchie und der Möglichkeit benutzerdefinierter Funktionsklassen auseinandersetzen.

Python-Funktionen

Für Python-Funktionen ändert sich im Wesentlichen nichts. Die zuvor vorhandenen Attribute sind weiterhin vorhanden, und Python-Funktionen können wie zuvor initialisiert, aufgerufen und in Methoden umgewandelt werden.

Der Name function wird aus Gründen der Abwärtskompatibilität beibehalten. Obwohl es sinnvoll wäre, den Namen in etwas Spezifischeres wie python_function zu ändern, würde dies viele lästige Änderungen in Dokumentation und Testsuiten erfordern.

Built-in-Funktionen eines Moduls

Auch für eingebaute Funktionen ändert sich nichts. Wir behalten das alte Verhalten bei, dass solche Funktionen nicht als Methoden gebunden werden. Dies ist eine Folge der Tatsache, dass __self__ auf das Modul gesetzt wird.

Built-in gebundene und ungebundene Methoden

Die Typen von eingebauten gebundenen und ungebundenen Methoden werden sich ändern. Dies beeinträchtigt jedoch nicht den Aufruf solcher Methoden, da das Protokoll in base_function.__call__ (insbesondere die Handhabung von __objclass__ und die Self-Slicing) speziell auf Abwärtskompatibilität ausgelegt war. Alle zuvor vorhandenen Attribute (wie __objclass__ und __self__) sind weiterhin vorhanden.

Neue Attribute

Einige Objekte erhalten neue spezielle Doppel-Unterstrich-Attribute. Zum Beispiel erscheint das neue Attribut __parent__ bei allen eingebauten Funktionen, und alle Methoden erhalten ein __func__ Attribut. Die Tatsache, dass __self__ nun ein spezielles schreibgeschütztes Attribut für Python-Funktionen ist, verursachte Probleme in [4]. Im Allgemeinen erwarten wir jedoch, dass nicht viel kaputt gehen wird.

method_descriptor und PyDescr_NewMethod

Die Klasse method_descriptor und der Konstruktor PyDescr_NewMethod sollten als veraltet markiert werden. Sie werden von CPython selbst nicht mehr verwendet, werden aber weiterhin unterstützt.

Zwei-Phasen-Implementierung

TODO: Dieser Abschnitt ist optional. Wenn dieses PEP akzeptiert wird, sollte entschieden werden, ob diese Zwei-Phasen-Implementierung angewendet wird oder nicht.

Wie oben erwähnt, können die Änderungen an types und inspect bestehenden Code brechen. Um den Bruch weiter zu minimieren, könnte dieses PEP in zwei Phasen implementiert werden.

Phase Eins: Bestehende Klassen beibehalten, aber Basisklassen hinzufügen

Zunächst wird die base_function Klasse implementiert und als gemeinsame Basisklasse verwendet, ansonsten werden jedoch die bestehenden Klassen (aber nicht ihre Implementierung) beibehalten.

In diesem Vorschlag würde die Klassenhierarchie zu

                      object
                         |
                         |
                  base_function
                 /       |     \
                /        |      \
               /         |       \
      cfunction          |     defined_function
       |     |           |         \
       |     |      bound_method    \
       |     |                       \
       |  method_descriptor       function
       |
builtin_function_or_method

Die Blattklassen builtin_function_or_method, method_descriptor, bound_method und function entsprechen den bestehenden Klassen (wobei method in bound_method umbenannt wird).

Automatisch erstellte Funktionen, die in Modulen erstellt werden, werden zu Instanzen von builtin_function_or_method. Ungebundene Methoden von Erweiterungstypen werden zu Instanzen von method_descriptor.

Die Klasse method_descriptor ist eine Kopie von cfunction, außer dass __get__ ein builtin_function_or_method anstelle eines bound_method zurückgibt.

Die Klasse builtin_function_or_method hat dieselbe C-Struktur wie ein bound_method, erbt aber von cfunction. Das Attribut __func__ ist nicht zwingend erforderlich: es wird nur definiert, wenn ein method_descriptor gebunden wird.

Wir behalten die Implementierung der inspect-Funktionen bei, wie sie sind. Aufgrund dessen und weil die vorhandenen Klassen beibehalten werden, ist die Abwärtskompatibilität für Code, der Typüberprüfungen durchführt, gewährleistet.

Da das Anzeigen einer tatsächlichen DeprecationWarning viel korrekt funktionierenden Code beeinträchtigen würde, würden Deprecations nur in der Dokumentation erscheinen. Ein weiterer Grund ist, dass es schwierig ist, Warnungen für den Aufruf von isinstance(x, t) anzuzeigen (obwohl dies mit __instancecheck__-Hacks möglich wäre) und unmöglich für type(x) is t.

Phase Zwei

Phase zwei ist das, was im Rest dieser PEP tatsächlich beschrieben wird. In Bezug auf die Implementierung wäre dies eine relativ kleine Änderung im Vergleich zu Phase eins.

Referenzimplementierung

Der Großteil dieser PEP wurde für CPython unter https://github.com/jdemeyer/cpython/tree/pep575 implementiert.

Es gibt vier Schritte, die den Commits auf diesem Branch entsprechen. Nach jedem Schritt ist CPython in einem weitgehend funktionierenden Zustand.

  1. Fügen Sie die Klasse base_function hinzu und machen Sie sie zu einer Unterklasse von cfunction. Dies ist mit Abstand der größte Schritt, da das vollständige __call__-Protokoll in diesem Schritt implementiert wird.
  2. Benennen Sie method in bound_method um und machen Sie sie zu einer Unterklasse von base_function. Ändern Sie nicht gebundene Methoden von Erweiterungstypen so, dass sie Instanzen von cfunction sind, sodass gebundene Methoden von Erweiterungstypen ebenfalls Instanzen von bound_method sind.
  3. Implementieren Sie defined_function und function.
  4. Änderungen an anderen Teilen von Python, wie der Standardbibliothek und der Testsuite.

Anhang: Aktuelle Situation

HINWEIS: Dieser Abschnitt ist während der Entwurfsphase der PEP nützlicher und kann daher entfernt werden, sobald die PEP akzeptiert wurde.

Zur Referenz beschreiben wir die relevanten bestehenden Klassen in CPython 3.7 im Detail.

Jede der beteiligten Klassen ist eine „Waisenklasse“ (keine nicht-trivialen Unter- oder Oberklassen).

builtin_function_or_method: Built-in-Funktionen und gebundene Methoden

Diese sind vom Typ PyCFunction_Type mit der Struktur PyCFunctionObject

typedef struct {
    PyObject_HEAD
    PyMethodDef *m_ml; /* Description of the C function to call */
    PyObject    *m_self; /* Passed as 'self' arg to the C func, can be NULL */
    PyObject    *m_module; /* The __module__ attribute, can be anything */
    PyObject    *m_weakreflist; /* List of weak references */
} PyCFunctionObject;

struct PyMethodDef {
    const char  *ml_name;   /* The name of the built-in function/method */
    PyCFunction ml_meth;    /* The C function that implements it */
    int         ml_flags;   /* Combination of METH_xxx flags, which mostly
                               describe the args expected by the C func */
    const char  *ml_doc;    /* The __doc__ attribute, or NULL */
};

wobei PyCFunction ein C-Funktionszeiger ist (es gibt verschiedene Formen davon, die grundlegendste nimmt zwei Argumente für self und *args).

Diese Klasse wird sowohl für Funktionen als auch für gebundene Methoden verwendet: für eine Methode zeigt der Slot m_self auf das Objekt

>>> dict(foo=42).get
<built-in method get of dict object at 0x...>
>>> dict(foo=42).get.__self__
{'foo': 42}

In einigen Fällen wird eine Funktion als „Methode“ des Moduls betrachtet, das sie definiert

>>> import os
>>> os.kill
<built-in function kill>
>>> os.kill.__self__
<module 'posix' (built-in)>

method_descriptor: Built-in ungebundene Methoden

Diese sind vom Typ PyMethodDescr_Type mit der Struktur PyMethodDescrObject

typedef struct {
    PyDescrObject d_common;
    PyMethodDef *d_method;
} PyMethodDescrObject;

typedef struct {
    PyObject_HEAD
    PyTypeObject *d_type;
    PyObject *d_name;
    PyObject *d_qualname;
} PyDescrObject;

function: Python-Funktionen

Diese sind vom Typ PyFunction_Type mit der Struktur PyFunctionObject

typedef struct {
    PyObject_HEAD
    PyObject *func_code;        /* A code object, the __code__ attribute */
    PyObject *func_globals;     /* A dictionary (other mappings won't do) */
    PyObject *func_defaults;    /* NULL or a tuple */
    PyObject *func_kwdefaults;  /* NULL or a dict */
    PyObject *func_closure;     /* NULL or a tuple of cell objects */
    PyObject *func_doc;         /* The __doc__ attribute, can be anything */
    PyObject *func_name;        /* The __name__ attribute, a string object */
    PyObject *func_dict;        /* The __dict__ attribute, a dict or NULL */
    PyObject *func_weakreflist; /* List of weak references */
    PyObject *func_module;      /* The __module__ attribute, can be anything */
    PyObject *func_annotations; /* Annotations, a dict or NULL */
    PyObject *func_qualname;    /* The qualified name */

    /* Invariant:
     *     func_closure contains the bindings for func_code->co_freevars, so
     *     PyTuple_Size(func_closure) == PyCode_GetNumFree(func_code)
     *     (func_closure may be NULL if PyCode_GetNumFree(func_code) == 0).
     */
} PyFunctionObject;

In Python 3 gibt es keine Klasse für „unbound methods“ (nicht gebundene Methoden): eine nicht gebundene Methode ist einfach eine normale Funktion.

method: Python gebundene Methoden

Diese sind vom Typ PyMethod_Type mit der Struktur PyMethodObject

typedef struct {
    PyObject_HEAD
    PyObject *im_func;   /* The callable object implementing the method */
    PyObject *im_self;   /* The instance it is bound to */
    PyObject *im_weakreflist; /* List of weak references */
} PyMethodObject;

Referenzen


Quelle: https://github.com/python/peps/blob/main/peps/pep-0575.rst

Zuletzt geändert: 2025-02-01 08:55:40 GMT