PEP 487 – Einfachere Anpassung der Klassenerstellung
- Autor:
- Martin Teichmann <lkb.teichmann at gmail.com>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 27. Feb 2015
- Python-Version:
- 3.6
- Post-History:
- 27. Feb 2015, 05. Feb 2016, 24. Jun 2016, 02. Jul 2016, 13. Jul 2016
- Ersetzt:
- 422
- Resolution:
- Python-Dev Nachricht
Zusammenfassung
Derzeit erfordert die Anpassung der Klassenerstellung die Verwendung einer benutzerdefinierten Metaklasse. Diese benutzerdefinierte Metaklasse bleibt für den gesamten Lebenszyklus der Klasse bestehen, was das Potenzial für irreführende Metaklassen-Konflikte birgt.
Dieser PEP schlägt vor, stattdessen eine breite Palette von Anpassungsszenarien durch einen neuen __init_subclass__ Hook im Klassenkörper und einen Hook zur Initialisierung von Attributen zu unterstützen.
Der neue Mechanismus sollte einfacher zu verstehen und zu verwenden sein als die Implementierung einer benutzerdefinierten Metaklasse und somit einen sanfteren Einstieg in die volle Leistungsfähigkeit der Metaklassen-Maschinerie von Python bieten.
Hintergrund
Metaklassen sind ein mächtiges Werkzeug zur Anpassung der Klassenerstellung. Sie haben jedoch das Problem, dass es keine automatische Möglichkeit gibt, Metaklassen zu kombinieren. Wenn man zwei Metaklassen für eine Klasse verwenden möchte, muss eine neue Metaklasse erstellt werden, die diese beiden kombiniert, typischerweise manuell.
Diese Notwendigkeit tritt oft überraschend für einen Benutzer auf: Das Erben von zwei Basisklassen, die aus zwei verschiedenen Bibliotheken stammen, erfordert plötzlich die manuelle Erstellung einer kombinierten Metaklasse, obwohl man sich im Allgemeinen nicht für diese Details der Bibliotheken interessiert. Dies wird noch schlimmer, wenn eine Bibliothek beginnt, eine Metaklasse zu verwenden, die sie zuvor nicht verwendet hat. Während die Bibliothek selbst perfekt weiter funktioniert, schlägt plötzlich jeder Code fehl, der diese Klassen mit Klassen aus einer anderen Bibliothek kombiniert.
Vorschlag
Obwohl es viele mögliche Wege gibt, eine Metaklasse zu verwenden, fallen die überwiegende Mehrheit der Anwendungsfälle in nur drei Kategorien: einige Initialisierungscodes, die nach der Klassenerstellung ausgeführt werden, die Initialisierung von Deskriptoren und die Beibehaltung der Reihenfolge, in der Klassenattribute definiert wurden.
Die ersten beiden Kategorien können leicht durch einfache Hooks in die Klassenerstellung erreicht werden
- Ein
__init_subclass__Hook, der alle Unterklassen einer gegebenen Klasse initialisiert. - Bei der Klassenerstellung wird ein
__set_name__Hook für alle Attribute (Deskriptoren) aufgerufen, die in der Klasse definiert sind, und
Die dritte Kategorie ist Thema eines anderen PEP, PEP 520.
Als Beispiel sieht der erste Anwendungsfall wie folgt aus
>>> class QuestBase:
... # this is implicitly a @classmethod (see below for motivation)
... def __init_subclass__(cls, swallow, **kwargs):
... cls.swallow = swallow
... super().__init_subclass__(**kwargs)
>>> class Quest(QuestBase, swallow="african"):
... pass
>>> Quest.swallow
'african'
Die Basisklasse object enthält eine leere __init_subclass__-Methode, die als Endpunkt für kooperative Mehrfachvererbung dient. Beachten Sie, dass diese Methode keine Schlüsselwortargumente hat, was bedeutet, dass alle spezialisierteren Methoden alle Schlüsselwortargumente verarbeiten müssen.
Dieser allgemeine Vorschlag ist keine neue Idee (er wurde erstmals vor über 10 Jahren für die Aufnahme in die Sprachdefinition vorgeschlagen und ein ähnlicher Mechanismus wird seit langem von Zope's ExtensionClass unterstützt), aber die Situation hat sich in den letzten Jahren ausreichend geändert, dass die Idee eine erneute Berücksichtigung wert ist.
Der zweite Teil des Vorschlags fügt einen __set_name__-Initialisierer für Klassenattribute hinzu, insbesondere wenn es sich um Deskriptoren handelt. Deskriptoren werden im Körper einer Klasse definiert, aber sie wissen nichts über diese Klasse, sie kennen nicht einmal den Namen, mit dem sie zugegriffen werden. Sie erfahren ihren Besitzer erst, wenn __get__ aufgerufen wird, aber immer noch kennen sie ihren Namen nicht. Das ist unglücklich, zum Beispiel können sie ihren zugehörigen Wert nicht unter ihrem Namen in das __dict__ ihres Objekts einfügen, da sie diesen Namen nicht kennen. Dieses Problem wurde schon oft gelöst und ist einer der wichtigsten Gründe, eine Metaklasse in einer Bibliothek zu haben. Obwohl es einfach wäre, einen solchen Mechanismus mit dem ersten Teil des Vorschlags zu implementieren, ist es sinnvoll, eine Lösung für dieses Problem für alle zu haben.
Um ein Beispiel für seine Verwendung zu geben, stellen Sie sich einen Deskriptor vor, der schwach referenzierte Werte darstellt
import weakref
class WeakAttribute:
def __get__(self, instance, owner):
return instance.__dict__[self.name]()
def __set__(self, instance, value):
instance.__dict__[self.name] = weakref.ref(value)
# this is the new initializer:
def __set_name__(self, owner, name):
self.name = name
Ein solcher WeakAttribute kann beispielsweise in einer Baumstruktur verwendet werden, in der man zyklische Referenzen über den Elternknoten vermeiden möchte
class TreeNode:
parent = WeakAttribute()
def __init__(self, parent):
self.parent = parent
Beachten Sie, dass das parent-Attribut wie ein normales Attribut verwendet wird, aber der Baum enthält keine zyklischen Referenzen und kann daher bei Nichtgebrauch leicht von der Garbage Collection bereinigt werden. Das parent-Attribut wird magischerweise None, sobald der Elternknoten nicht mehr existiert.
Obwohl dieses Beispiel sehr trivial aussieht, sollte beachtet werden, dass bis jetzt ein solches Attribut nicht ohne die Verwendung einer Metaklasse definiert werden konnte. Und da eine solche Metaklasse das Leben sehr schwer machen kann, gibt es diese Art von Attribut bisher nicht.
Die Initialisierung von Deskriptoren könnte einfach im __init_subclass__ Hook erfolgen. Das würde aber bedeuten, dass Deskriptoren nur in Klassen verwendet werden können, die den richtigen Hook haben; die generische Version wie im Beispiel würde nicht allgemein funktionieren. Man könnte __set_name__ auch aus der Basisimplementierung von object.__init_subclass__ aufrufen. Da es jedoch ein häufiger Fehler ist, den Aufruf von super() zu vergessen, würde es zu oft passieren, dass Deskriptoren nicht initialisiert werden.
Hauptvorteile
Einfachere Vererbung von Verhaltensweisen zur Definitionszeit
Das Verständnis von Pythons Metaklassen erfordert ein tiefes Verständnis des Typsystems und des Klassenerstellungsprozesses. Dies wird zu Recht als herausfordernd angesehen, da mehrere bewegliche Teile (der Code, der Metaklassen-Hinweis, die tatsächliche Metaklasse, das Klassenobjekt, Instanzen des Klassenobjekts) klar auseinandergehalten werden müssen. Selbst wenn man die Regeln kennt, ist es immer noch leicht, einen Fehler zu machen, wenn man nicht extrem vorsichtig ist.
Das Verständnis des vorgeschlagenen impliziten Klasseninitialisierungs-Hooks erfordert nur normale Methodenvererbung, was keine so einschüchternde Aufgabe ist. Der neue Hook bietet einen graduelleren Weg zum Verständnis aller Phasen des Klassendefinitionsprozesses.
Reduziertes Risiko von Metaklassen-Konflikten
Eines der großen Probleme, die Bibliotheksautoren zögern lassen, Metaklassen zu verwenden (auch wenn sie angebracht wären), ist das Risiko von Metaklassen-Konflikten. Diese treten auf, wenn zwei nicht zusammenhängende Metaklassen von den gewünschten Eltern einer Klassendefinition verwendet werden. Dieses Risiko macht es auch sehr schwierig, einer Klasse, die zuvor keine Metaklasse hatte, eine Metaklasse *hinzuzufügen*.
Im Gegensatz dazu birgt das Hinzufügen einer __init_subclass__-Methode zu einem bestehenden Typ ein ähnliches Risiko wie das Hinzufügen einer __init__-Methode: Technisch gesehen besteht das Risiko, schlecht implementierte Unterklassen zu brechen, aber wenn dies geschieht, wird es als Fehler in der Unterklasse anerkannt und nicht als Bruch von Abwärtskompatibilitätsgarantien durch den Bibliotheksautor.
Neue Wege zur Nutzung von Klassen
Registrierung von Unterklassen
Insbesondere beim Schreiben eines Plugin-Systems möchte man neue Unterklassen einer Plugin-Basisklasse registrieren. Dies kann wie folgt geschehen
class PluginBase:
subclasses = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.subclasses.append(cls)
In diesem Beispiel enthält PluginBase.subclasses eine einfache Liste aller Unterklassen im gesamten Vererbungsbaum. Es ist zu beachten, dass dies auch gut als Mixin-Klasse funktioniert.
Trait-Deskriptoren
Es gibt viele Designs von Python-Deskriptoren, die beispielsweise Grenzwerte von Werten prüfen. Oft benötigen diese "Traits" eine Unterstützung einer Metaklasse, um zu funktionieren. So würde das mit diesem PEP aussehen
class Trait:
def __init__(self, minimum, maximum):
self.minimum = minimum
self.maximum = maximum
def __get__(self, instance, owner):
return instance.__dict__[self.key]
def __set__(self, instance, value):
if self.minimum < value < self.maximum:
instance.__dict__[self.key] = value
else:
raise ValueError("value not in range")
def __set_name__(self, owner, name):
self.key = name
Implementierungsdetails
Die Hooks werden in der folgenden Reihenfolge aufgerufen: type.__new__ ruft die __set_name__ Hooks auf dem Deskriptor auf, nachdem die neue Klasse initialisiert wurde. Dann ruft es __init_subclass__ auf der Basisklasse auf, um genau zu sein, auf super(). Das bedeutet, dass Unterklassen-Initialisierer die vollständig initialisierten Deskriptoren bereits sehen. Auf diese Weise können __init_subclass__-Benutzer alle Deskriptoren bei Bedarf erneut reparieren.
Eine andere Option wäre gewesen, __set_name__ in der Basisimplementierung von object.__init_subclass__ aufzurufen. Auf diese Weise wäre es sogar möglich gewesen, den Aufruf von __set_name__ zu verhindern. Meistens wäre eine solche Verhinderung jedoch zufällig, da es oft vorkommt, dass ein Aufruf von super() vergessen wird.
Als dritte Option könnte die gesamte Arbeit in type.__init__ erledigt worden sein. Die meisten Metaklassen führen ihre Arbeit in __new__ aus, da dies von der Dokumentation empfohlen wird. Viele Metaklassen modifizieren ihre Argumente, bevor sie sie an super().__new__ weitergeben. Um mit diesen Arten von Klassen kompatibel zu sein, sollten die Hooks aus __new__ aufgerufen werden.
Eine weitere kleine Änderung sollte vorgenommen werden: In der aktuellen Implementierung von CPython verbietet type.__init__ ausdrücklich die Verwendung von Schlüsselwortargumenten, während type.__new__ die Übergabe seiner Attribute als Schlüsselwortargumente zulässt. Dies ist seltsam inkohärent und sollte daher verboten werden. Obwohl es möglich wäre, das aktuelle Verhalten beizubehalten, wäre es besser, dies zu beheben, da es wahrscheinlich überhaupt nicht verwendet wird: Der einzige Anwendungsfall wäre, dass eine Metaklasse ihr super().__new__ mit _name_, _bases_ und _dict_ (ja, _dict_, nicht _namespace_ oder _ns_ wie meistens bei modernen Metaklassen verwendet) als Schlüsselwortargumente aufruft. Dies sollte nicht geschehen. Diese kleine Änderung vereinfacht die Implementierung dieses PEP erheblich und verbessert gleichzeitig die Kohärenz von Python insgesamt.
Als zweite Änderung ignoriert die neue type.__init__ einfach Schlüsselwortargumente. Derzeit besteht sie darauf, dass keine Schlüsselwortargumente gegeben werden. Dies führt zu einem (gewollten) Fehler, wenn man Schlüsselwortargumente für eine Klassendeklaration übergibt und die Metaklasse sie nicht verarbeitet. Metaklassenautoren, die Schlüsselwortargumente akzeptieren möchten, müssen sie durch Überschreiben von __init__ herausfiltern.
Im neuen Code ist es nicht __init__, das sich über Schlüsselwortargumente beschwert, sondern __init_subclass__, dessen Standardimplementierung keine Argumente entgegennimmt. In einem klassischen Vererbungsschema mit der Method Resolution Order kann jede __init_subclass__ ihre Schlüsselwortargumente herausnehmen, bis keine mehr übrig sind, was von der Standardimplementierung von __init_subclass__ überprüft wird.
Für Leser, die lieber Python als Englisch lesen, schlägt dieser PEP vor, das aktuelle type und object durch das Folgende zu ersetzen
class NewType(type):
def __new__(cls, *args, **kwargs):
if len(args) != 3:
return super().__new__(cls, *args)
name, bases, ns = args
init = ns.get('__init_subclass__')
if isinstance(init, types.FunctionType):
ns['__init_subclass__'] = classmethod(init)
self = super().__new__(cls, name, bases, ns)
for k, v in self.__dict__.items():
func = getattr(v, '__set_name__', None)
if func is not None:
func(self, k)
super(self, self).__init_subclass__(**kwargs)
return self
def __init__(self, name, bases, ns, **kwargs):
super().__init__(name, bases, ns)
class NewObject(object):
@classmethod
def __init_subclass__(cls):
pass
Referenzimplementierung
Die Referenzimplementierung für diesen PEP ist an Issue 27366 angehängt.
Probleme mit der Abwärtskompatibilität
Die genaue Aufrufabfolge in type.__new__ ist leicht geändert, was Befürchtungen hinsichtlich der Abwärtskompatibilität aufkommen lässt. Tests sollten sicherstellen, dass gängige Anwendungsfälle wie gewünscht funktionieren.
Die folgenden Klassendefinitionen (außer derjenigen, die die Metaklasse definiert) schlagen weiterhin mit einem TypeError fehl, da überflüssige Klassenargumente übergeben werden
class MyMeta(type):
pass
class MyClass(metaclass=MyMeta, otherarg=1):
pass
MyMeta("MyClass", (), otherargs=1)
import types
types.new_class("MyClass", (), dict(metaclass=MyMeta, otherarg=1))
types.prepare_class("MyClass", (), dict(metaclass=MyMeta, otherarg=1))
Eine Metaklasse, die nur eine __new__-Methode definiert, die an Schlüsselwortargumenten interessiert ist, muss nun keine __init__-Methode mehr definieren, da die Standard-type.__init__ Schlüsselwortargumente ignoriert. Dies steht schön im Einklang mit der Empfehlung, in Metaklassen __new__ anstelle von __init__ zu überschreiben. Der folgende Code schlägt nicht mehr fehl
class MyMeta(type):
def __new__(cls, name, bases, namespace, otherarg):
return super().__new__(cls, name, bases, namespace)
class MyClass(metaclass=MyMeta, otherarg=1):
pass
Nur die Definition einer __init__-Methode in einer Metaklasse schlägt weiterhin mit TypeError fehl, wenn Schlüsselwortargumente übergeben werden
class MyMeta(type):
def __init__(self, name, bases, namespace, otherarg):
super().__init__(name, bases, namespace)
class MyClass(metaclass=MyMeta, otherarg=1):
pass
Die Definition von sowohl __init__ als auch __new__ funktioniert weiterhin problemlos.
Das Einzige, was nicht mehr funktioniert, ist die Übergabe der Argumente von type.__new__ als Schlüsselwortargumente
class MyMeta(type):
def __new__(cls, name, bases, namespace):
return super().__new__(cls, name=name, bases=bases,
dict=namespace)
class MyClass(metaclass=MyMeta):
pass
Dies führt nun zu einem TypeError, aber das ist seltsamer Code und leicht zu beheben, selbst wenn jemand diese Funktion genutzt hat.
Abgelehnte Designoptionen
Aufruf des Hooks an der Klasse selbst
Das Hinzufügen eines __autodecorate__-Hooks, der an der Klasse selbst aufgerufen würde, war die Idee von PEP 422. Die meisten Beispiele funktionieren gleich oder sogar besser, wenn der Hook nur auf strikte Unterklassen angewendet wird. Im Allgemeinen ist es viel einfacher, den Hook explizit auf die Klasse aufzurufen, in der er definiert ist (um sich für ein solches Verhalten zu entscheiden), als sich davon abzumelden (indem man sich daran erinnert, cls is __class im Hook-Körper zu überprüfen), was bedeutet, dass man nicht möchte, dass der Hook für die Klasse aufgerufen wird, in der er definiert ist.
Dies wird am deutlichsten, wenn die betreffende Klasse als Mixin konzipiert ist: Es ist sehr unwahrscheinlich, dass der Code des Mixins für die Mixin-Klasse selbst ausgeführt werden soll, da sie nicht als eigenständige vollständige Klasse gedacht ist.
Der ursprüngliche Vorschlag nahm auch größere Änderungen am Klassenerstellungsprozess vor, was es unmöglich machte, den Vorschlag auf ältere Python-Versionen zurückzuportieren.
Wenn der Hook auch für die Basisklasse aufgerufen werden soll, stehen zwei Mechanismen zur Verfügung
- Einführung einer zusätzlichen Mixin-Klasse nur zur Aufnahme der
__init_subclass__-Implementierung. Die ursprüngliche "Basis"-Klasse kann dann die neue Mixin als ihre erste Elternklasse auflisten. - Implementierung des gewünschten Verhaltens als unabhängiger Klassen-Dekorator und Anwendung dieses Dekorators explizit auf die Basisklasse und dann implizit auf Unterklassen über
__init_subclass__.
Das explizite Aufrufen von __init_subclass__ aus einem Klassen-Dekorator ist im Allgemeinen unerwünscht, da dies normalerweise auch __init_subclass__ ein zweites Mal für die Elternklasse aufrufen würde, was wahrscheinlich nicht das gewünschte Verhalten ist.
Andere Varianten des Aufrufs der Hooks
Andere Namen für den Hook wurden präsentiert, nämlich __decorate__ oder __autodecorate__. Dieser Vorschlag entscheidet sich für __init_subclass__, da es dem __init__-Methode sehr ähnlich ist, nur für die Unterklasse, während es nicht sehr ähnlich zu Dekoratoren ist, da es die Klasse nicht zurückgibt.
Für den __set_name__ Hook wurden ebenfalls andere Namen vorgeschlagen, wie __set_owner__, __set_ownership__ und __init_descriptor__.
Erfordert einen expliziten Dekorator auf __init_subclass__
Man könnte die explizite Verwendung von @classmethod auf dem __init_subclass__-Decorator verlangen. Es wurde implizit gemacht, da es keine sinnvolle Interpretation gibt, es wegzulassen, und dieser Fall ohnehin erkannt werden müsste, um eine aussagekräftige Fehlermeldung zu geben.
Diese Entscheidung wurde bekräftigt, nachdem festgestellt wurde, dass die Benutzererfahrung beim Definieren von __prepare__ und dem Vergessen des @classmethod-Methodendekorators einzigartig unverständlich ist (insbesondere da PEP 3115 ihn als gewöhnliche Methode dokumentiert und die aktuelle Dokumentation nichts Eindeutiges dazu sagt).
Ein __new__-ähnlicherer Hook
In PEP 422 funktionierte der Hook eher wie die __new__-Methode als die __init__-Methode, d.h. er gab eine Klasse zurück, anstatt eine zu modifizieren. Dies ermöglicht etwas mehr Flexibilität, aber auf Kosten einer viel schwierigeren Implementierung und unerwünschten Nebenwirkungen.
Hinzufügen eines Klassenattributs mit der Attributreihenfolge
Dies erhielt seinen eigenen PEP 520.
Historie
Dies war ein konkurrierender Vorschlag zu PEP 422 von Alyssa Coghlan und Daniel Urban. PEP 422 zielte darauf ab, die gleichen Ziele wie dieser PEP zu erreichen, jedoch mit einer anderen Implementierungsweise. In der Zwischenzeit wurde PEP 422 zurückgezogen, zugunsten dieses Ansatzes.
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0487.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT