PEP 560 – Kernunterstützung für das Modul `typing` und generische Typen
- Autor:
- Ivan Levkivskyi <levkivskyi at gmail.com>
- Status:
- Final
- Typ:
- Standards Track
- Thema:
- Typisierung
- Erstellt:
- 03-Sep-2017
- Python-Version:
- 3.7
- Post-History:
- 09-Sep-2017, 14-Nov-2017
- Resolution:
- Python-Dev Nachricht
Zusammenfassung
Ursprünglich wurde PEP 484 so konzipiert, dass sie **keine** Änderungen am CPython-Kerninterpreter einführt. Mittlerweile werden Typ-Hints und das Modul typing von der Community ausgiebig genutzt. So erweitern PEP 526 und PEP 557 die Nutzung von Typ-Hints, und der Backport von typing auf PyPI hat 1 Mio. Downloads/Monat. Daher kann diese Einschränkung aufgehoben werden. Es wird vorgeschlagen, zwei spezielle Methoden __class_getitem__ und __mro_entries__ in den CPython-Kern aufzunehmen, um generische Typen besser zu unterstützen.
Begründung
Die Einschränkung, den CPython-Kerninterpreter nicht zu ändern, führte zu einigen Designentscheidungen, die fragwürdig wurden, als das Modul typing weithin genutzt wurde. Es gibt drei Hauptprobleme: die Leistung des Moduls typing, Metaklassenkonflikte und die große Anzahl von Hacks, die derzeit in typing verwendet werden.
Performance
Das Modul typing ist eines der schwersten und langsamsten Module in der Standardbibliothek, selbst mit allen vorgenommenen Optimierungen. Dies liegt hauptsächlich daran, dass subscripted generische Typen (siehe PEP 484 für die Definition der in dieser PEP verwendeten Begriffe) Klassenobjekte sind (siehe auch [1]). Mit Hilfe der vorgeschlagenen speziellen Methoden können die Leistung auf drei Hauptarten verbessert werden:
- Die Erstellung generischer Klassen ist langsam, da
GenericMeta.__new__sehr langsam ist; wir werden sie nicht mehr benötigen. - Sehr lange Method Resolution Orders (MROs) für generische Klassen werden halb so lang sein; sie sind vorhanden, weil wir die Vererbungskette von
collections.abcintypingduplizieren. - Die Instanziierung generischer Klassen wird schneller sein (dies ist jedoch von untergeordneter Bedeutung).
Metaklassenkonflikte
Alle generischen Typen sind Instanzen von GenericMeta. Wenn also ein Benutzer eine benutzerdefinierte Metaklasse verwendet, ist es schwierig, eine entsprechende Klasse generisch zu machen. Dies ist besonders schwierig für Bibliotheksklassen, die ein Benutzer nicht kontrolliert. Eine Problemumgehung besteht darin, immer GenericMeta zu mixen.
class AdHocMeta(GenericMeta, LibraryMeta):
pass
class UserClass(LibraryBase, Generic[T], metaclass=AdHocMeta):
...
aber das ist nicht immer praktikabel oder sogar möglich. Mit Hilfe der vorgeschlagenen speziellen Attribute wird die Metaklasse GenericMeta nicht mehr benötigt.
Hacks und Fehler, die durch diesen Vorschlag entfernt werden
_generic_new-Hack, der existiert, weil__init__auf Instanzen mit einem Typ, der sich vom Typ unterscheidet, dessen__new__aufgerufen wurde, nicht aufgerufen wird;C[int]().__class__ is C._next_in_mro-Geschwindigkeitshack wird nicht mehr notwendig sein, da die Subskription keine neuen Klassen erstellen wird.- Hässlicher
sys._getframe-Hack. Dieser ist besonders heimtückisch, da es so aussieht, als könnten wir ihn nicht ohne Änderungen außerhalb vontypingentfernen. - Derzeit nehmen Generics gefährliche Dinge mit privaten ABC-Caches vor, um einen großen Speicherverbrauch zu beheben, der mindestens mit O(N^2) wächst, siehe [2]. Dieser Punkt ist auch wichtig, da kürzlich vorgeschlagen wurde,
ABCMetain C neu zu implementieren. - Probleme beim Teilen von Attributen zwischen subscripted Generics, siehe [3]. Die aktuelle Lösung verwendet bereits
__getattr__und__setattr__, ist aber immer noch unvollständig. Die Lösung dieses Problems ohne den aktuellen Vorschlag wäre schwierig und würde__getattribute__benötigen. _no_slots_copy-Hack, bei dem wir das Klassendiktionär bei jeder Subskription bereinigen, was Generics mit__slots__ermöglicht.- Allgemeine Komplexität des Moduls
typing. Der neue Vorschlag wird nicht nur die Entfernung der oben genannten Hacks/Fehler ermöglichen, sondern auch die Implementierung vereinfachen, so dass sie leichter zu warten ist.
Spezifikation
__class_getitem__
Die Idee von __class_getitem__ ist einfach: Sie ist ein exaktes Analogon zu __getitem__, mit der Ausnahme, dass sie auf einer Klasse aufgerufen wird, die sie definiert, nicht auf ihren Instanzen. Dies ermöglicht es uns, GenericMeta.__getitem__ für Dinge wie Iterable[int] zu vermeiden. __class_getitem__ ist automatisch eine Klassenmethode und erfordert keine @classmethod-Dekoration (ähnlich wie __init_subclass__) und wird wie normale Attribute vererbt. Zum Beispiel:
class MyList:
def __getitem__(self, index):
return index + 1
def __class_getitem__(cls, item):
return f"{cls.__name__}[{item.__name__}]"
class MyOtherList(MyList):
pass
assert MyList()[0] == 1
assert MyList[int] == "MyList[int]"
assert MyOtherList()[0] == 1
assert MyOtherList[int] == "MyOtherList[int]"
Beachten Sie, dass diese Methode als Fallback verwendet wird. Wenn also eine Metaklasse __getitem__ definiert, hat diese Vorrang.
__mro_entries__
Wenn ein Objekt, das kein Klassenobjekt ist, im Tupel der Basisklassen einer Klassendefinition erscheint, wird die Methode __mro_entries__ darauf gesucht. Wenn sie gefunden wird, wird sie mit dem ursprünglichen Tupel der Basisklassen als Argument aufgerufen. Das Ergebnis des Aufrufs muss ein Tupel sein, das anstelle dieses Objekts in die Basisklassen entpackt wird. (Wenn das Tupel leer ist, bedeutet dies, dass die ursprünglichen Basisklassen einfach verworfen werden.) Wenn es mehr als ein Objekt mit __mro_entries__ gibt, werden alle mit demselben ursprünglichen Tupel von Basisklassen aufgerufen. Dieser Schritt erfolgt zuerst im Prozess der Klassenerstellung, alle anderen Schritte, einschließlich der Prüfungen auf doppelte Basisklassen und der MRO-Berechnung, erfolgen normal mit den aktualisierten Basisklassen.
Die Verwendung einer Methoden-API anstelle eines reinen Attributs ist notwendig, um inkonsistente MRO-Fehler zu vermeiden und andere Manipulationen durchzuführen, die derzeit von GenericMeta.__new__ durchgeführt werden. Die ursprünglichen Basisklassen werden als __orig_bases__ im Klassennamenraum gespeichert (derzeit wird dies auch von der Metaklasse getan). Zum Beispiel:
class GenericAlias:
def __init__(self, origin, item):
self.origin = origin
self.item = item
def __mro_entries__(self, bases):
return (self.origin,)
class NewList:
def __class_getitem__(cls, item):
return GenericAlias(cls, item)
class Tokens(NewList[int]):
...
assert Tokens.__bases__ == (NewList,)
assert Tokens.__orig_bases__ == (NewList[int],)
assert Tokens.__mro__ == (Tokens, NewList, object)
Die Auflösung mittels __mro_entries__ erfolgt **nur** in den Basisklassen einer Klassendefinitionsanweisung. In allen anderen Situationen, in denen ein Klassenobjekt erwartet wird, erfolgt keine solche Auflösung, dies schließt die eingebauten Funktionen isinstance und issubclass ein.
HINWEIS: Diese beiden Methodennamen sind für die Verwendung durch das Modul typing und die generische Typenmaschinerie reserviert, und jede andere Verwendung wird nicht empfohlen. Die Referenzimplementierung (mit Tests) finden Sie unter [4], und der Vorschlag wurde ursprünglich im typing-Tracker veröffentlicht und diskutiert, siehe [5].
Dynamische Klassenerstellung und types.resolve_bases
type.__new__ führt keine MRO-Eintragungsauflösung durch. Eine direkte Aufrufung type('Tokens', (List[int],), {}) wird daher fehlschlagen. Dies geschieht aus Leistungsgründen und um die Anzahl impliziter Transformationen zu minimieren. Stattdessen wird eine Hilfsfunktion resolve_bases dem Modul types hinzugefügt, um eine explizite __mro_entries__-Auflösung im Kontext der dynamischen Klassenerstellung zu ermöglichen. Entsprechend wird types.new_class aktualisiert, um die neuen Klassenerstellungsschritte widerzuspiegeln und gleichzeitig die Abwärtskompatibilität zu gewährleisten.
def new_class(name, bases=(), kwds=None, exec_body=None):
resolved_bases = resolve_bases(bases) # This step is added
meta, ns, kwds = prepare_class(name, resolved_bases, kwds)
if exec_body is not None:
exec_body(ns)
ns['__orig_bases__'] = bases # This step is added
return meta(name, resolved_bases, ns, **kwds)
Verwendung von __class_getitem__ in C-Erweiterungen
Wie oben erwähnt, ist __class_getitem__ automatisch eine Klassenmethode, wenn sie in Python-Code definiert ist. Um diese Methode in einer C-Erweiterung zu definieren, sollte man die Flags METH_O|METH_CLASS verwenden. Ein einfacher Weg, eine Erweiterungsklasse generisch zu machen, ist beispielsweise die Verwendung einer Methode, die einfach das ursprüngliche Klassenobjekt zurückgibt, wodurch die Typinformationen zur Laufzeit vollständig gelöscht werden und alle Prüfungen nur an statische Typ-Checker delegiert werden.
typedef struct {
PyObject_HEAD
/* ... your code ... */
} SimpleGeneric;
static PyObject *
simple_class_getitem(PyObject *type, PyObject *item)
{
Py_INCREF(type);
return type;
}
static PyMethodDef simple_generic_methods[] = {
{"__class_getitem__", simple_class_getitem, METH_O|METH_CLASS, NULL},
/* ... other methods ... */
};
PyTypeObject SimpleGeneric_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
"SimpleGeneric",
sizeof(SimpleGeneric),
0,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_methods = simple_generic_methods,
};
Eine solche Klasse kann als normale generische Klasse in Python-Typannotationen verwendet werden (eine entsprechende Stub-Datei sollte für statische Typ-Checker bereitgestellt werden, siehe PEP 484 für Details).
from simple_extension import SimpleGeneric
from typing import TypeVar
T = TypeVar('T')
Alias = SimpleGeneric[str, T]
class SubClass(SimpleGeneric[T, int]):
...
data: Alias[int] # Works at runtime
more_data: SubClass[str] # Also works at runtime
Abwärtskompatibilität und Auswirkungen auf Benutzer, die typing nicht verwenden
Dieser Vorschlag kann Code brechen, der derzeit die Namen __class_getitem__ und __mro_entries__ verwendet. (Die Sprachreferenz reserviert jedoch ausdrücklich **alle** undokumentierten Dunder-Namen und erlaubt "Bruch ohne Vorwarnung"; siehe [6].)
Dieser Vorschlag unterstützt eine fast vollständige Abwärtskompatibilität mit der aktuellen öffentlichen API für generische Typen; darüber hinaus ist das Modul typing immer noch vorläufig. Die einzigen beiden Ausnahmen sind, dass derzeit issubclass(List[int], List) True zurückgibt, während es mit diesem Vorschlag TypeError auslösen wird, und dass repr() von nicht-subscripted benutzerdefinierten generischen Typen nicht angepasst werden kann und mit repr() von normalen (nicht-generischen) Klassen übereinstimmen wird.
Mit der Referenzimplementierung habe ich vernachlässigbare Leistungseffekte (weniger als 1 % in einem Micro-Benchmark) für reguläre (nicht-generische) Klassen gemessen. Gleichzeitig hat sich die Leistung von generischen Typen erheblich verbessert:
importlib.reload(typing)ist bis zu 7x schneller.- Die Erstellung benutzerdefinierter generischer Klassen ist bis zu 4x schneller (in einem Micro-Benchmark mit leerem Körper).
- Die Instanziierung generischer Klassen ist bis zu 5x schneller (in einem Micro-Benchmark mit leerem
__init__). - Andere Operationen mit generischen Typen und Instanzen (wie Methodenauflösung und
isinstance()-Prüfungen) sind um etwa 10-20 % verbessert. - Der einzige Aspekt, der sich mit der aktuellen Proof-of-Concept-Implementierung verschlechtert, ist die Cache-Suche für subscripted Generics. Allerdings war diese bereits sehr effizient, so dass dieser Aspekt einen vernachlässigbaren Gesamteinfluss hat.
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0560.rst
Zuletzt geändert: 2024-06-11 22:12:09 GMT