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

Python Enhancement Proposals

PEP 737 – C API zur Formatierung eines vollqualifizierten Typnamens

Autor:
Victor Stinner <vstinner at python.org>
Discussions-To:
Discourse thread
Status:
Final
Typ:
Standards Track
Erstellt:
29. Nov. 2023
Python-Version:
3.13
Post-History:
29. Nov. 2023
Resolution:
Discourse-Nachricht

Inhaltsverzeichnis

Zusammenfassung

Hinzufügen neuer praktischer C-APIs zur Formatierung eines vollqualifizierten Typnamens. Typnamen nicht mehr unterschiedlich formatieren, je nachdem, wie Typen implementiert sind.

Empfehlen Sie die Verwendung des vollqualifizierten Typnamens in Fehlermeldungen und in __repr__()-Methoden in neuem C-Code. Empfehlen Sie, Typnamen in neuem C-Code nicht abzuschneiden.

Fügen Sie die Formate %T, %#T, %N und %#N zu PyUnicode_FromFormat() hinzu, um die vollqualifizierten Namen eines Objekttyps bzw. eines Typs zu formatieren.

Machen Sie C-Code sicherer, indem Sie ausgeliehene Referenzen vermeiden, die zu Abstürzen führen können. Die neue C-API ist mit der begrenzten C-API kompatibel.

Begründung

Standardbibliothek

In der Python-Standardbibliothek ist die Formatierung eines Typnamens oder des Typnamens eines Objekts eine häufige Operation zur Formatierung einer Fehlermeldung und zur Implementierung einer __repr__()-Methode. Es gibt verschiedene Möglichkeiten, einen Typnamen zu formatieren, die unterschiedliche Ausgaben liefern.

Beispiel mit dem Typ datetime.timedelta

  • Der kurze Typname (type.__name__) und der qualifizierte Typname (type.__qualname__) sind 'timedelta'.
  • Das Typmodul (type.__module__) ist 'datetime'.
  • Der vollqualifizierte Typname ist 'datetime.timedelta'.
  • Die Typdarstellung (repr(type)) enthält den vollqualifizierten Namen: <class 'datetime.timedelta'>.

Python-Code

In Python erhält type.__name__ den kurzen Typnamen, während f"{type.__module__}.{type.__qualname__}" den „vollqualifizierten Namen“ des Typs formatiert. Normalerweise werden type(obj) oder obj.__class__ verwendet, um den Typ des Objekts obj zu erhalten. Manchmal wird der Typname in Anführungszeichen gesetzt.

Beispiele

  • raise TypeError("str erwartet, nicht %s" % type(value).__name__)
  • raise TypeError("kann %s nicht serialisieren" % self.__class__.__name__)
  • name = "%s.%s" % (obj.__module__, obj.__qualname__)

Qualifizierte Namen wurden in Python 3.3 durch PEP 3155 „Qualified name for classes and functions“ zu Typen hinzugefügt (type.__qualname__).

C-Code

In C ist die gebräuchlichste Methode zur Formatierung eines Typnamens der Zugriff auf das Mitglied PyTypeObject.tp_name des Typs. Beispiel

PyErr_Format(PyExc_TypeError, "globals must be a dict, not %.100s",
             Py_TYPE(globals)->tp_name);

Der „vollqualifizierte Typname“ wird an wenigen Stellen verwendet: PyErr_Display(), die Implementierung von type.__repr__() und die Implementierung von sys.unraisablehook.

Die Verwendung von Py_TYPE(obj)->tp_name wird bevorzugt, da sie bequemer ist als der Aufruf von PyType_GetQualName(), der ein Py_DECREF() erfordert. Außerdem wurde PyType_GetQualName() erst kürzlich, in Python 3.11, hinzugefügt.

Einige Funktionen verwenden %R (repr(type)) zur Formatierung eines Typnamens, die Ausgabe enthält den vollqualifizierten Typnamen. Beispiel

PyErr_Format(PyExc_TypeError,
             "calling %R should have returned an instance "
             "of BaseException, not %R",
             type, Py_TYPE(value));

Verwendung von PyTypeObject.tp_name ist inkonsistent mit Python

Das Mitglied PyTypeObject.tp_name unterscheidet sich je nach Typimplementierung

  • Statische Typen und Heap-Typen in C: *tp_name* ist der vollqualifizierte Typname.
  • Python-Klasse: *tp_name* ist der kurze Typname (type.__name__).

Die Verwendung von Py_TYPE(obj)->tp_name zur Formatierung eines Objekttyppnamens ergibt also eine andere Ausgabe, je nachdem, ob ein Typ in C oder in Python implementiert ist.

Dies widerspricht den Prinzipien von PEP 399 „Pure Python/C Accelerator Module Compatibility Requirements“, die empfehlen, dass sich Code gleich verhält, wenn er in Python oder in C geschrieben ist.

Beispiel

$ python3.12
>>> import _datetime; c_obj = _datetime.date(1970, 1, 1)
>>> import _pydatetime; py_obj = _pydatetime.date(1970, 1, 1)
>>> my_list = list(range(3))

>>> my_list[c_obj]  # C type
TypeError: list indices must be integers or slices, not datetime.date

>>> my_list[py_obj]  # Python type
TypeError: list indices must be integers or slices, not date

Die Fehlermeldung enthält den vollqualifizierten Typnamen (datetime.date), wenn der Typ in C implementiert ist, oder den kurzen Typnamen (date), wenn der Typ in Python implementiert ist.

Begrenzte C-API

Der Code Py_TYPE(obj)->tp_name kann nicht mit der begrenzten C-API verwendet werden, da die Mitglieder von PyTypeObject von der begrenzten C-API ausgeschlossen sind.

Der Typname sollte mit den Funktionen PyType_GetName(), PyType_GetQualName() und PyType_GetModule() gelesen werden, die weniger bequem zu verwenden sind.

Abschneiden von Typnamen in C

1998, als die Funktion PyErr_Format() hinzugefügt wurde, verwendete die Implementierung einen festen Puffer von 500 Bytes. Die Funktion hatte folgenden Kommentar

/* Caller is responsible for limiting the format */

2001 wurde die Funktion geändert, um einen dynamischen Puffer auf dem Heap zuzuweisen. Zu spät, die Praxis des Abschneidens von Typnamen, wie die Verwendung des Formats %.100s, wurde bereits zur Gewohnheit, und Entwickler vergaßen, warum Typnamen abgeschnitten werden. In Python werden Typnamen nicht abgeschnitten.

Das Abschneiden von Typnamen in C, aber nicht in Python, widerspricht den Prinzipien von PEP 399 „Pure Python/C Accelerator Module Compatibility Requirements“, die empfehlen, dass sich Code gleich verhält, wenn er in Python oder in C geschrieben ist.

Siehe das Issue: Replace %.100s by %s in PyErr_Format(): the arbitrary limit of 500 bytes is outdated (2011).

Spezifikation

  • Hinzufügen der Funktion PyType_GetFullyQualifiedName().
  • Hinzufügen der Funktion PyType_GetModuleName().
  • Hinzufügen von Formaten zu PyUnicode_FromFormat().
  • Empfehlen Sie die Verwendung des vollqualifizierten Typnamens in Fehlermeldungen und in __repr__()-Methoden in neuem C-Code.
  • Empfehlen Sie, Typnamen in neuem C-Code nicht abzuschneiden.

Hinzufügen der Funktion PyType_GetFullyQualifiedName()

Fügen Sie die Funktion PyType_GetFullyQualifiedName() hinzu, um den vollqualifizierten Namen eines Typs zu erhalten: ähnlich wie f"{type.__module__}.{type.__qualname__}", oder type.__qualname__, wenn type.__module__ kein String ist oder gleich "builtins" oder "__main__" ist.

API

PyObject* PyType_GetFullyQualifiedName(PyTypeObject *type)

Bei Erfolg wird eine neue Referenz auf den String zurückgegeben. Im Fehlerfall wird eine Ausnahme ausgelöst und NULL zurückgegeben.

Hinzufügen der Funktion PyType_GetModuleName()

Fügen Sie die Funktion PyType_GetModuleName() hinzu, um den Modulnamen eines Typs (type.__module__ String) zu erhalten. API

PyObject* PyType_GetModuleName(PyTypeObject *type)

Bei Erfolg wird eine neue Referenz auf den String zurückgegeben. Im Fehlerfall wird eine Ausnahme ausgelöst und NULL zurückgegeben.

Hinzufügen von Formaten zu PyUnicode_FromFormat()

Fügen Sie die folgenden Formate zu PyUnicode_FromFormat() hinzu

  • %N formatiert den **vollqualifizierten Namen** eines **Typs**, ähnlich wie PyType_GetFullyQualifiedName(type); **N** steht für den Typ**n**amen.
  • %T formatiert den **vollqualifizierten Typnamen** eines Objekt**t**yps, ähnlich wie PyType_GetFullyQualifiedName(Py_TYPE(obj)); **T** steht für Objek**t**yp.
  • %#N und %#T: Die alternative Form verwendet den Doppelpunkt-Separator (:) anstelle des Punkt-Separators (.) zwischen dem Modulnamen und dem qualifizierten Namen.

Beispielsweise kann der vorhandene Code, der *tp_name* verwendet

PyErr_Format(PyExc_TypeError,
             "__format__ must return a str, not %.200s",
             Py_TYPE(result)->tp_name);

mit dem %T-Format ersetzt werden

PyErr_Format(PyExc_TypeError,
             "__format__ must return a str, not %T", result);

Vorteile des aktualisierten Codes

  • Sicherer C-Code: Vermeiden Sie Py_TYPE(), das eine ausgeliehene Referenz zurückgibt.
  • Das Mitglied PyTypeObject.tp_name wird nicht mehr explizit gelesen: Der Code wird mit der begrenzten C-API kompatibel.
  • Der formatierte Typname hängt nicht mehr von der Typimplementierung ab.
  • Der Typname wird nicht mehr abgeschnitten.

Hinweis: Das Format %T wird von time.strftime() verwendet, aber nicht von printf().

Zusammenfassung der Formate

C-Objekt C-Typ Format
%T %N Typ **vollqualifizierter** Name.
%#T %#N Typ **vollqualifizierter** Name, **Doppelpunkt**-Separator.

Empfehlen Sie die Verwendung des vollqualifizierten Typnamens

Der vollqualifizierte Typname wird in Fehlermeldungen und in __repr__()-Methoden in neuem C-Code empfohlen.

In nicht trivialen Anwendungen ist es wahrscheinlich, dass zwei Typen mit demselben kurzen Namen in zwei verschiedenen Modulen definiert sind, insbesondere bei generischen Namen. Die Verwendung des vollqualifizierten Namens hilft, den Typ eindeutig zu identifizieren.

Empfehlen Sie, Typnamen nicht abzuschneiden

Typnamen sollten in neuem C-Code nicht abgeschnitten werden. Zum Beispiel sollte das Format %.100s vermieden werden: Verwenden Sie stattdessen das Format %s (oder %T-Format in C).

Implementierung

Abwärtskompatibilität

Die in diesem PEP vorgeschlagenen Änderungen sind abwärtskompatibel.

Das Hinzufügen neuer C-APIs hat keine Auswirkungen auf die Abwärtskompatibilität. Bestehende C-APIs bleiben unverändert. Es werden keine Python-APIs geändert.

Der Ersatz des kurzen Typnamens durch den vollqualifizierten Typnamen wird nur in neuem C-Code empfohlen. Das Nicht-Abschneiden von Typnamen wird ebenfalls nur in neuem C-Code empfohlen. Bestehender Code sollte unverändert bleiben und ist somit abwärtskompatibel. Es gibt keine Empfehlung für Python-Code.

Abgelehnte Ideen

Hinzufügen des Attributs type.__fully_qualified_name__

Fügen Sie das schreibgeschützte Attribut type.__fully_qualified_name__ hinzu, den vollqualifizierten Namen eines Typs: ähnlich wie f"{type.__module__}.{type.__qualname__}", oder type.__qualname__, wenn type.__module__ kein String ist oder gleich "builtins" oder "__main__" ist.

Die type.__repr__()-Methode bleibt unverändert, sie lässt das Modul nur weg, wenn das Modul gleich "builtins" ist.

Diese Änderung wurde vom Steering Council abgelehnt

Wir erkennen die Nützlichkeit der von PEP vorgeschlagenen C-API-Änderungen und würden diese Änderungen wahrscheinlich so übernehmen.

Wir sehen weniger Rechtfertigung für die Änderungen auf Python-Ebene. Insbesondere hinterfragen wir die Notwendigkeit von __fully_qualified_name__.

Thomas Wouters fügte hinzu

Wenn es tatsächlich den Wunsch gibt, Typen exakt so zu formatieren, wie es die C-API tut, wäre eine Hilfsfunktion meiner persönlichen Meinung nach sinnvoller als type.__format__, aber ich denke, das SC könnte mit einigen konkreten Anwendungsfällen überzeugt werden.

Hinzufügen der Methode type.__format__()

Fügen Sie die Methode type.__format__() mit den folgenden Formaten hinzu

  • N formatiert den **vollqualifizierten Namen** des Typs (type.__fully_qualified_name__); N steht für **N**ame.
  • #N (alternative Form) formatiert den **vollqualifizierten Namen** des Typs unter Verwendung des Doppelpunkt-Separators (:) anstelle des Punkt-Separators (.) zwischen dem Modulnamen und dem qualifizierten Namen.

Beispiele mit f-Strings

>>> import datetime
>>> f"{datetime.timedelta:N}"  # fully qualified name
'datetime.timedelta'
>>> f"{datetime.timedelta:#N}" # fully qualified name, colon separator
'datetime:timedelta'

Der Doppelpunkt-Separator (:), der vom Format #N verwendet wird, eliminiert Rätselraten, wenn Sie den Namen importieren möchten, siehe pkgutil.resolve_name(), die Befehlszeilenschnittstelle python -m inspect und setuptools-Einstiegspunkte.

Diese Änderung wurde vom Steering Council abgelehnt.

Änderung von str(type)

Die Methode type.__str__() kann geändert werden, um einen Typnamen unterschiedlich zu formatieren. Zum Beispiel kann sie den vollqualifizierten Typnamen zurückgeben.

Das Problem ist, dass es sich um eine abwärts inkompatible Änderung handelt. Zum Beispiel müssten die Module enum, functools, optparse, pdb und xmlrpc.server der Standardbibliothek aktualisiert werden. Auch die Tests test_dataclasses, test_descrtut und test_cmd_line_script müssten aktualisiert werden.

Siehe den Pull-Request: type(str) returns the fully qualified name.

Hinzufügen des !t-Formatierers, um einen Objekttyp zu erhalten

Verwenden Sie f"{obj!t:T}", um type(obj).__fully_qualified_name__ zu formatieren, ähnlich wie f"{type(obj):T}".

Als der !t-Formatierer 2018 vorgeschlagen wurde, war Eric Smith stark dagegen; Eric ist der Autor des f-string PEP 498 „Literal String Interpolation“.

Hinzufügen von Formaten zu str % args

Es wurde vorgeschlagen, Formate hinzuzufügen, um einen Typnamen in str % arg zu formatieren. Zum Beispiel die Hinzufügung des Formats %T, um einen vollqualifizierten Typnamen zu formatieren.

Heutzutage werden f-Strings für neuen Code bevorzugt.

Andere Möglichkeiten zur Formatierung von Typnamen in C

Die Funktion printf() unterstützt mehrere Größenmodifikatoren: hh (char), h (short), l (long), ll (long long), z (size_t), t (ptrdiff_t) und j (intmax_t). Die Funktion PyUnicode_FromFormat() unterstützt die meisten davon.

Vorgeschlagene Formate unter Verwendung von h und hh Längenmodifikatoren

  • %hhT formatiert type.__name__.
  • %hT formatiert type.__qualname__.
  • %T formatiert type.__fully_qualified_name__.

Längenmodifikatoren werden verwendet, um den C-Typ des Arguments anzugeben, nicht um zu ändern, wie ein Argument formatiert wird. Die alternative Form (#) ändert, wie ein Argument formatiert wird. Hier ist der C-Typ des Arguments immer PyObject*.

Andere vorgeschlagene Formate

  • %Q
  • %t.
  • %lT formatiert type.__fully_qualified_name__.
  • %Tn formatiert type.__name__.
  • %Tq formatiert type.__qualname__.
  • %Tf formatiert type.__fully_qualified_name__.

Mehr Optionen zur Formatierung von Typnamen können zu Inkonsistenzen zwischen verschiedenen Modulen führen und die API fehleranfälliger machen.

Bezüglich des Formats %t verwendet printf() jetzt t als Längenmodifikator für ptrdiff_t-Argumente.

Die folgenden APIs, die zur Formatierung eines Typs verwendet werden sollen

C API Python API Format
PyType_GetName() type.__name__ Typ **kurzer** Name.
PyType_GetQualName() type.__qualname__ Typ **qualifizierter** Name.
PyType_GetModuleName() type.__module__ Typ **Modul**name.

Verwenden Sie das Format %T mit Py_TYPE(): Übergabe eines Typs

Es wurde vorgeschlagen, einen Typ an das Format %T zu übergeben, wie

PyErr_Format(PyExc_TypeError, "object type name: %T", Py_TYPE(obj));

Die Funktion Py_TYPE() gibt eine ausgeliehene Referenz zurück. Nur zur Formatierung eines Fehlers scheint die Verwendung einer ausgeliehenen Referenz auf einen Typ sicher zu sein. In der Praxis kann dies zu Abstürzen führen. Beispiel

import gc
import my_cext

class ClassA:
    pass

def create_object():
     class ClassB:
          def __repr__(self):
                self.__class__ = ClassA
                gc.collect()
                return "ClassB repr"
     return ClassB()

obj = create_object()
my_cext.func(obj)

wobei my_cext.func() eine C-Funktion ist, die

PyErr_Format(PyExc_ValueError,
             "Unexpected value %R of type %T",
             obj, Py_TYPE(obj));

PyErr_Format() mit einer ausgeliehenen Referenz auf ClassB aufgerufen wird. Wenn repr(obj) vom Format %R aufgerufen wird, wird die letzte Referenz auf ClassB entfernt und die Klasse freigegeben. Wenn das Format %T verarbeitet wird, ist Py_TYPE(obj) bereits ein verwaister Zeiger und Python stürzt ab.

Andere vorgeschlagene APIs zum Abrufen eines vollqualifizierten Typnamens

  • Fügen Sie das Attribut type.__fullyqualname__ hinzu: Name ohne Unterstrich zwischen Wörtern. Mehrere Dunder, darunter einige der zuletzt hinzugefügten, enthalten einen Unterstrich im Wort: __class_getitem__, __release_buffer__, __type_params__, __init_subclass__ und __text_signature__.
  • Fügen Sie das Attribut type.__fqn__ hinzu: FQN steht für **F**ully **Q**ualified **N**ame.
  • Fügen Sie die Methode type.fully_qualified_name() hinzu. Methoden, die zu type hinzugefügt werden, werden von allen Typen geerbt und können daher bestehenden Code beeinflussen.
  • Fügen Sie eine Funktion zum inspect-Modul hinzu. Sie müssen das inspect-Modul importieren, um es zu verwenden.

Einschließen des __main__-Moduls in den vollqualifizierten Typnamen

Formatieren Sie type.__fully_qualified_name__ als f"{type.__module__}.{type.__qualname__}" oder type.__qualname__, wenn type.__module__ kein String ist oder gleich "builtins" ist. Behandeln Sie das Modul __main__ nicht anders: Schließen Sie es in den Namen ein.

Bestehender Code wie type.__repr__(), die Module collections.abc und unittest formatieren einen Typnamen mit f'{obj.__module__}.{obj.__qualname__}' und lassen den Modulteil nur weg, wenn das Modul gleich builtins ist.

Nur die Module traceback und pdb lassen das Modul auch weg, wenn es gleich "builtins" oder "__main__" ist.

Das Attribut type.__fully_qualified_name__ lässt das Modul __main__ weg, um kürzere Namen für einen häufigen Fall zu erzeugen: Typen, die in einem Skript definiert sind, das mit python script.py ausgeführt wird. Zum Debuggen kann die Funktion repr() für einen Typ verwendet werden, sie schließt das Modul __main__ in den Typnamen ein. Oder verwenden Sie das Format f"{type.__module__}.{type.__qualname__}", um den Modulnamen immer einzuschließen, auch für das Modul "builtins".

Beispiel-Skript

class MyType:
    pass

print(f"name: {MyType.__fully_qualified_name__}")
print(f"repr: {repr(MyType)}")

Ausgabe

name: MyType
repr: <class '__main__.MyType'>

Diskussionen


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

Zuletzt geändert: 2024-06-01 20:53:34 GMT