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

Python Enhancement Proposals

PEP 674 – Makros nicht als l-Werte zulassen

Autor:
Victor Stinner <vstinner at python.org>
Status:
Verschoben
Typ:
Standards Track
Erstellt:
30. Nov. 2021
Python-Version:
3.12

Inhaltsverzeichnis

Zusammenfassung

Verwendung von Makros als l-Werte nicht zulassen. Zum Beispiel schlägt Py_TYPE(obj) = new_type nun mit einem Compilerfehler fehl.

In der Praxis müssen die meisten betroffenen Projekte nur zwei Änderungen vornehmen

  • Ersetzen Sie Py_TYPE(obj) = new_type durch Py_SET_TYPE(obj, new_type).
  • Ersetzen Sie Py_SIZE(obj) = new_size durch Py_SET_SIZE(obj, new_size).

PEP Verschiebung

Siehe SC-Antwort auf PEP 674 – Makros nicht als l-Werte zulassen (Februar 2022).

Begründung

Verwendung eines Makros als l-Wert

In der Python C API sind einige Funktionen als Makros implementiert, da die Erstellung eines Makros einfacher ist als die einer regulären Funktion. Wenn ein Makro direkt ein Strukturmitglied offenlegt, ist es technisch möglich, dieses Makro nicht nur zum Abrufen des Strukturmitglieds, sondern auch zum Setzen zu verwenden.

Beispiel mit dem Python 3.10 Py_TYPE() Makro

#define Py_TYPE(ob) (((PyObject *)(ob))->ob_type)

Dieses Makro kann als r-Wert verwendet werden, um den Typ eines Objekts zu ermitteln

type = Py_TYPE(object);

Es kann auch als l-Wert verwendet werden, um den Typ eines Objekts zu setzen

Py_TYPE(object) = new_type;

Es ist auch möglich, die Referenzanzahl und die Größe eines Objekts mithilfe der Makros Py_REFCNT() und Py_SIZE() zu setzen.

Das direkte Setzen eines Objektattributs hängt von der aktuellen genauen CPython-Implementierung ab. Die Implementierung dieser Funktion in anderen Python-Implementierungen kann deren C-API-Implementierung weniger effizient machen.

CPython nogil fork

Sam Gross hat Python 3.9 geforkt, um den GIL zu entfernen: den nogil-Branch. Dieser Fork hat kein PyObject.ob_refcnt-Mitglied, sondern eine aufwendigere Implementierung für die Referenzzählung, und daher schlägt der Code Py_REFCNT(obj) = new_refcnt; mit einem Compilerfehler fehl.

Das Zusammenführen des nogil-Forks in den Haupt-Branch des Upstream-CPython erfordert zunächst die Behebung dieses Kompatibilitätsproblems der C-API. Es ist ein konkretes Beispiel für eine Python-Optimierung, die indirekt durch die C-API blockiert wird.

Dieses Problem wurde bereits in Python 3.10 behoben: das Makro Py_REFCNT() wurde bereits so modifiziert, dass seine Verwendung als l-Wert nicht mehr zulässig ist.

Diese Aussagen werden von Sam Gross (nogil-Entwickler) unterstützt.

HPy-Projekt

Das HPy-Projekt ist eine brandneue C-API für Python, die nur Handles und Funktionsaufrufe verwendet: Handles sind undurchsichtig, Strukturmitglieder können nicht direkt zugegriffen werden und Zeiger können nicht dereferenziert werden.

Das Suchen und Ersetzen von Py_SET_SIZE() ist einfacher und sicherer als das Suchen und Ersetzen einiger seltsamer Makroverwendungen von Py_SIZE(). Py_SIZE() kann semi-mechanisch durch HPy_Length() ersetzt werden, während das Sehen von Py_SET_SIZE() sofort deutlich machen würde, dass der Code größere Änderungen erfordert, um nach HPy portiert zu werden (z. B. durch die Verwendung von HPyTupleBuilder oder HPyListBuilder).

Je weniger interne Details über Makros offengelegt werden, desto einfacher wird es für HPy sein, direkte Entsprechungen anzubieten. Jedes Makro, das auf „nicht öffentliche“ Schnittstellen verweist, macht diese Schnittstellen effektiv öffentlich.

Diese Aussagen werden von Antonio Cuni (HPy-Entwickler) unterstützt.

GraalVM Python

In GraalVM muss die C-API-Emulationsschicht bei Zugriff auf ein Python-Objekt durch die Python C API die GraalVM-Objekte in Wrapper verpacken, die die interne Struktur der CPython-Strukturen (PyObject, PyLongObject, PyTypeObject usw.) offenlegen. Dies liegt daran, dass wenn der C-Code direkt oder über Makros darauf zugreift, alles, was GraalVM abfangen kann, ein Lesen an einem Offset der Struktur ist, das auf die Darstellung in GraalVM zurückgeführt werden muss. Je kleiner die „effektive“ Anzahl der offengelegten Strukturmitglieder ist (durch Ersetzen von Makros durch Funktionen), desto einfacher können die GraalVM-Wrapper sein.

Diese PEP allein reicht nicht aus, um die Wrapper in GraalVM abzuschaffen, aber sie ist ein Schritt in Richtung dieses langfristigen Ziels. GraalVM unterstützt bereits HPy, was langfristig eine bessere Lösung ist.

Diese Aussagen werden von Tim Felgentreff (GraalVM Python-Entwickler) unterstützt.

Spezifikation

Makros nicht als l-Werte verwenden

Die folgenden 65 Makros werden geändert, um ihre Verwendung als l-Werte nicht mehr zuzulassen.

PyObject- und PyVarObject-Makros

  • Py_TYPE(): stattdessen muss Py_SET_TYPE() verwendet werden
  • Py_SIZE(): stattdessen muss Py_SET_SIZE() verwendet werden

GET-Makros

  • PyByteArray_GET_SIZE()
  • PyBytes_GET_SIZE()
  • PyCFunction_GET_CLASS()
  • PyCFunction_GET_FLAGS()
  • PyCFunction_GET_FUNCTION()
  • PyCFunction_GET_SELF()
  • PyCell_GET()
  • PyCode_GetNumFree()
  • PyDict_GET_SIZE()
  • PyFunction_GET_ANNOTATIONS()
  • PyFunction_GET_CLOSURE()
  • PyFunction_GET_CODE()
  • PyFunction_GET_DEFAULTS()
  • PyFunction_GET_GLOBALS()
  • PyFunction_GET_KW_DEFAULTS()
  • PyFunction_GET_MODULE()
  • PyHeapType_GET_MEMBERS()
  • PyInstanceMethod_GET_FUNCTION()
  • PyList_GET_SIZE()
  • PyMemoryView_GET_BASE()
  • PyMemoryView_GET_BUFFER()
  • PyMethod_GET_FUNCTION()
  • PyMethod_GET_SELF()
  • PySet_GET_SIZE()
  • PyTuple_GET_SIZE()
  • PyUnicode_GET_DATA_SIZE()
  • PyUnicode_GET_LENGTH()
  • PyUnicode_GET_LENGTH()
  • PyUnicode_GET_SIZE()
  • PyWeakref_GET_OBJECT()

AS-Makros

  • PyByteArray_AS_STRING()
  • PyBytes_AS_STRING()
  • PyFloat_AS_DOUBLE()
  • PyUnicode_AS_DATA()
  • PyUnicode_AS_UNICODE()

PyUnicode-Makros

  • PyUnicode_1BYTE_DATA()
  • PyUnicode_2BYTE_DATA()
  • PyUnicode_4BYTE_DATA()
  • PyUnicode_DATA()
  • PyUnicode_IS_ASCII()
  • PyUnicode_IS_COMPACT()
  • PyUnicode_IS_READY()
  • PyUnicode_KIND()
  • PyUnicode_READ()
  • PyUnicode_READ_CHAR()

PyDateTime GET-Makros

  • PyDateTime_DATE_GET_FOLD()
  • PyDateTime_DATE_GET_HOUR()
  • PyDateTime_DATE_GET_MICROSECOND()
  • PyDateTime_DATE_GET_MINUTE()
  • PyDateTime_DATE_GET_SECOND()
  • PyDateTime_DATE_GET_TZINFO()
  • PyDateTime_DELTA_GET_DAYS()
  • PyDateTime_DELTA_GET_MICROSECONDS()
  • PyDateTime_DELTA_GET_SECONDS()
  • PyDateTime_GET_DAY()
  • PyDateTime_GET_MONTH()
  • PyDateTime_GET_YEAR()
  • PyDateTime_TIME_GET_FOLD()
  • PyDateTime_TIME_GET_HOUR()
  • PyDateTime_TIME_GET_MICROSECOND()
  • PyDateTime_TIME_GET_MINUTE()
  • PyDateTime_TIME_GET_SECOND()
  • PyDateTime_TIME_GET_TZINFO()

C-Erweiterungen nach Python 3.11 portieren

In der Praxis müssen die meisten von diesen PEP betroffenen Projekte nur zwei Änderungen vornehmen

  • Ersetzen Sie Py_TYPE(obj) = new_type durch Py_SET_TYPE(obj, new_type).
  • Ersetzen Sie Py_SIZE(obj) = new_size durch Py_SET_SIZE(obj, new_size).

Das pythoncapi_compat-Projekt kann verwendet werden, um C-Erweiterungen automatisch zu aktualisieren: Python 3.11-Unterstützung hinzufügen, ohne die Unterstützung für ältere Python-Versionen zu verlieren. Das Projekt stellt eine Header-Datei zur Verfügung, die Py_SET_REFCNT(), Py_SET_TYPE() und Py_SET_SIZE() Funktionen für Python 3.8 und älter bereitstellt.

PyTuple_GET_ITEM() und PyList_GET_ITEM() bleiben unverändert

Die Makros PyTuple_GET_ITEM() und PyList_GET_ITEM() bleiben unverändert.

Die Code-Muster &PyTuple_GET_ITEM(tuple, 0) und &PyList_GET_ITEM(list, 0) werden immer noch häufig verwendet, um auf das innere PyObject**-Array zuzugreifen.

Die Änderung dieser Makros liegt außerhalb des Geltungsbereichs dieser PEP.

PyDescr_NAME() und PyDescr_TYPE() bleiben unverändert

Die Makros PyDescr_NAME() und PyDescr_TYPE() bleiben unverändert.

Diese Makros geben Zugriff auf die Mitglieder PyDescrObject.d_name und PyDescrObject.d_type. Sie können als l-Werte verwendet werden, um diese Mitglieder zu setzen.

Das SWIG-Projekt verwendet diese Makros als l-Werte, um diese Mitglieder zu setzen. Es wäre möglich, SWIG zu ändern, um das direkte Setzen von PyDescrObject-Strukturmitgliedern zu verhindern, aber es lohnt sich nicht wirklich, da die PyDescrObject-Struktur nicht leistungskritisch ist und sich wahrscheinlich bald nicht mehr ändern wird.

Siehe das bpo-46538 „[C API] PyDescrObject-Struktur opak machen: PyDescr_NAME() und PyDescr_TYPE()“ für weitere Details.

Implementierung

Die Implementierung wird von bpo-45476: [C API] PEP 674: Verwendung von Makros als l-Werte nicht zulassen verfolgt.

Py_TYPE() und Py_SIZE() Makros

Im Mai 2020 wurden die Makros Py_TYPE() und Py_SIZE() geändert, um ihre Verwendung als l-Werte nicht zuzulassen (Py_TYPE, Py_SIZE).

Im November 2020 wurde die Änderung revertiert, da sie zu viele Drittanbieterprojekte kaputt machte.

Im Juni 2021, nachdem die meisten Drittanbieterprojekte aktualisiert wurden, wurde ein zweiter Versuch unternommen, der jedoch wieder revertiert werden musste, da er test_exceptions unter Windows kaputtmachte.

Im September 2021, nachdem test_exceptions behoben wurde, wurden Py_TYPE() und Py_SIZE() schließlich geändert.

Im November 2021 erhielt diese abwärtskompatible Änderung eine Ausnahmeregelung des Steering Council.

Im Oktober 2022 wurde Python 3.11 mit den abwärtsinkompatiblen Änderungen von Py_TYPE() und Py_SIZE() veröffentlicht.

Abwärtskompatibilität

Die vorgeschlagenen C-API-Änderungen sind absichtlich abwärtsinkompatibel.

In der Praxis werden nur die Makros Py_TYPE() und Py_SIZE() als l-Werte verwendet.

Diese Änderung folgt nicht dem PEP 387-Deprekationsprozess. Es gibt keine bekannte Möglichkeit, eine Deprekationswarnung nur dann auszugeben, wenn ein Makro als l-Wert verwendet wird, aber nicht, wenn es anders verwendet wird (z. B. als r-Wert).

Die folgenden 4 Makros bleiben unverändert, um die Anzahl der betroffenen Projekte zu reduzieren: PyDescr_NAME(), PyDescr_TYPE(), PyList_GET_ITEM() und PyTuple_GET_ITEM().

Statistiken

Insgesamt (Projekte auf PyPI und nicht auf PyPI) sind 34 Projekte von dieser PEP betroffen

  • 16 Projekte (47%) sind bereits behoben
  • 18 Projekte (53%) sind noch nicht behoben (fixen ausstehend oder Cython-Code muss neu generiert werden)

Am 1. September 2022 sind 18 Projekte (0,4 %) der Top 5000 PyPI-Projekte von der PEP betroffen

  • 15 Projekte (0,3 %) müssen ihren Cython-Code neu generieren
  • 3 Projekte (0,1 %) haben eine ausstehende Korrektur

Top 5000 PyPI

Projekte mit ausstehender Korrektur (3)

  • datatable (1.0.0): behoben
  • guppy3 (3.1.2): behoben
  • scipy (1.9.3): Boost Python muss aktualisiert werden

Darüber hinaus müssen 15 Projekte ihren Cython-Code neu generieren.

Projekte mit einer behobenen Version (12)

Es gibt auch zwei Backport-Projekte, die von dieser PEP betroffen sind

  • pickle5 (0.0.12): Backport für Python <= 3.7
  • pysha3 (1.0.2): Backport für Python <= 3.5

Sie dürfen und können auf Python 3.11 nicht verwendet werden.

Weitere betroffene Projekte

Andere Projekte mit einer behobenen Version (4)

Beziehung zum HPy-Projekt

Das HPy-Projekt

Die Hoffnung mit dem HPy-Projekt ist, eine C-API bereitzustellen, die der ursprünglichen API nahekommt – um die Portierung zu erleichtern – und deren Leistung nahe an der bestehenden API liegt. Gleichzeitig ist HPy ausreichend abgetrennt, um eine gute „C-Erweiterungs-API“ (im Gegensatz zu einer stabilen Teilmenge der CPython-Implementierungs-API) zu sein, die keine Implementierungsdetails preisgibt. Um diese letztere Eigenschaft zu gewährleisten, versucht das HPy-Projekt, alles parallel für CPython, PyPy und GraalVM Python zu entwickeln.

HPy entwickelt sich immer noch sehr schnell weiter. Probleme werden noch gelöst, während NumPy migriert wird, und es wurden Arbeiten begonnen, um HPy-Unterstützung zu Cython hinzuzufügen. Die Arbeit an pybind11 beginnt bald. Tim Felgentreff glaubt, dass HPy bis zu dem Zeitpunkt, an dem diese Benutzer der bestehenden C-API funktionieren, so weit sein wird, dass es allgemein nützlich ist und als stabil genug erachtet werden kann, dass die weitere Entwicklung einem stabileren Prozess folgen kann.

Langfristig möchte das HPy-Projekt eine geförderte API für die Erstellung von Python-C-Erweiterungen werden.

Das HPy-Projekt ist eine gute Lösung für die Zukunft. Es hat den Vorteil, außerhalb von Python entwickelt zu werden und erfordert keine Änderungen an der C-API.

Die C-API wird noch einige Jahre bestehen bleiben

Das erste Bedenken bezüglich HPy ist, dass HPy derzeit nicht ausgereift oder weit verbreitet ist und CPython weiterhin eine große Menge an C-Erweiterungen unterstützen muss, die wahrscheinlich nicht bald nach HPy portiert werden.

Die zweite Sorge ist die Unfähigkeit, CPython-Interna zu entwickeln, um neue Optimierungen zu implementieren, und die ineffiziente Implementierung der aktuellen C-API in PyPy, GraalPython usw. Leider wird HPy diese Probleme erst lösen, wenn die meisten C-Erweiterungen vollständig nach HPy portiert sind: Wenn es vernünftig wird, die „Legacy“-Python-C-API abzuschaffen.

Während die Portierung einer C-Erweiterung auf HPy auf CPython inkrementell erfolgen kann, erfordert sie die Änderung vieler Codezeilen und dauert Zeit. Die Portierung der meisten C-Erweiterungen auf HPy wird voraussichtlich einige Jahre dauern.

Diese PEP schlägt vor, die C-API „weniger schlecht“ zu machen, indem ein Problem behoben wird, das eindeutig als Ursache für praktische Probleme identifiziert wurde: Makros, die als l-Werte verwendet werden. Diese PEP erfordert nur die Aktualisierung einer Minderheit von C-Erweiterungen, und normalerweise müssen nur wenige Zeilen in den betroffenen Erweiterungen geändert werden.

Beispielsweise besteht NumPy 1.22 aus 307.300 Zeilen C-Code, und die Anpassung von NumPy an diese PEP hat nur 11 Zeilen geändert (Verwendung von Py_SET_TYPE und Py_SET_SIZE) und 4 Zeilen hinzugefügt (zur Definition von Py_SET_TYPE und Py_SET_SIZE für Python 3.8 und älter). Die Anfänge der NumPy-Portierung auf HPy erforderten bereits die Änderung von mehr Zeilen.

Derzeit ist es schwer zu sagen, welcher Ansatz der beste ist: die aktuelle C-API korrigieren oder sich auf HPy konzentrieren. Es wäre riskant, sich nur auf HPy zu konzentrieren.

Abgelehnte Idee: Makros unverändert lassen

Die Dokumentation jeder Funktion kann Entwickler davon abhalten, Makros zur Änderung von Python-Objekten zu verwenden.

Wenn eine Zuweisung erforderlich ist, kann eine Setter-Funktion hinzugefügt werden und die Dokumentation des Makros kann verlangen, die Setter-Funktion zu verwenden. Beispielsweise wurde eine Py_SET_TYPE()-Funktion zu Python 3.9 hinzugefügt und die Dokumentation von Py_TYPE() verlangt nun die Verwendung der Py_SET_TYPE()-Funktion, um einen Objekttyp zu setzen.

Wenn Entwickler Makros als l-Werte verwenden, liegt die Verantwortung für den Codebruch bei ihnen, nicht bei Python. Wir operieren nach dem Prinzip der einwilligenden Erwachsenen: Wir erwarten, dass Benutzer der Python C API sie wie dokumentiert verwenden und dass sie die Folgen tragen, wenn Dinge kaputt gehen, wenn sie dies nicht tun.

Diese Idee wurde abgelehnt, da nur wenige Entwickler die Dokumentation lesen und nur eine Minderheit die Änderungen an der Python C API-Dokumentation verfolgt. Die Mehrheit der Entwickler nutzt nur CPython und ist sich daher nicht bewusst über Kompatibilitätsprobleme mit anderen Python-Implementierungen.

Darüber hinaus hilft die weitere Zulassung der Verwendung von Makros als l-Werte dem HPy-Projekt nicht und überlässt die Last der Emulation der Python-Implementierung von GraalVM.

Bereits geänderte Makros

Die folgenden C API-Makros wurden bereits geändert, um ihre Verwendung als l-Werte nicht zuzulassen

  • PyCell_SET()
  • PyList_SET_ITEM()
  • PyTuple_SET_ITEM()
  • Py_REFCNT() (Python 3.10): stattdessen muss Py_SET_REFCNT() verwendet werden
  • _PyGCHead_SET_FINALIZED()
  • _PyGCHead_SET_NEXT()
  • asdl_seq_GET()
  • asdl_seq_GET_UNTYPED()
  • asdl_seq_LEN()
  • asdl_seq_SET()
  • asdl_seq_SET_UNTYPED()

Zum Beispiel schlägt PyList_SET_ITEM(list, 0, item) < 0 nun wie erwartet mit einem Compilerfehler fehl.

Post-Historie

Referenzen

Versionshistorie

  • Version 3: PyDescr_TYPE() und PyDescr_NAME() Makros werden nicht mehr geändert
  • Version 2: Abschnitt „Beziehung zum HPy-Projekt“ hinzufügen, PyPy-Abschnitt entfernen
  • Version 1: Erste öffentliche Version

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

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