Die Methode __prepare__ wurde in Python 3.0 über PEP 3115 eingeführt, um grundlegende Einschränkungen im Klassencreation-Protokoll zu adressieren. Vor dieser Änderung war der Namensraum, der während der Ausführung des Klassenkörpers verwendet wurde, immer ein Standard-Dictionary, das keine Möglichkeit bot, die Reihenfolge der Attributdeklarationen zu bewahren oder Zuweisungen abzuhandeln, während sie stattfanden. Dies wurde besonders problematisch für Entwickler von ORMs und Serialisierungsbibliotheken, die die Reihenfolge der Felderklärungen nachverfolgen mussten, ohne auf fragliches Quellcode-Parsing zurückzugreifen.
Wenn Python einen Klassenkörper ausführt, füllt es eine Namensraumzuordnung, die schließlich zum __dict__ der Klasse wird. Der Standardtyp dict garantiert in älteren Python-Versionen keine Einfügereihenfolge und bietet keine Hooks, um Namen zum Zeitpunkt ihrer Definition zu validieren oder zu transformieren. Entwickler, die Beschränkungen in der Deklarationszeit benötigen – wie das Verbot bestimmter Namensmuster oder das Nachverfolgen der Feldreihenfolge für binäre Protokolle – hatten keinen sauberen Mechanismus, um in dieser spezifischen Phase der Klassenkonstruktion einzugreifen, bevor das Klassenobjekt finalisiert wurde.
Durch die Implementierung von __prepare__ als statische Methode in einer Metaklasse kannst du eine benutzerdefinierte veränderbare Zuordnung (wie collections.OrderedDict oder ein benutzerdefiniertes validierendes Dictionary) zurückgeben, das als Namensraum dient. Diese Zuordnung erfasst alle Assignments auf Klassenebene während der Ausführung des Körpers und ermöglicht eine Vorverarbeitung, bevor die Methode __new__ der Metaklasse die Klasse finalisiert. Der benutzerdefinierte Namensraum wird dann an __new__ übergeben, wo er in ein Standard-dict umgewandelt oder für die geordnete Verwendung beibehalten werden kann.
from collections import OrderedDict class OrderPreservingMeta(type): @staticmethod def __prepare__(name, bases, **kwargs): return OrderedDict() def __new__(mcs, name, bases, namespace, **kwargs): ordered_attrs = list(namespace.keys()) cls = super().__new__(mcs, name, bases, dict(namespace)) cls._declaration_order = ordered_attrs return cls class Schema(metaclass=OrderPreservingMeta): id = 1 name = "test" value = 3.14 print(Schema._declaration_order) # ['id', 'name', 'value']
Eine Finanzhandelsplattform musste binäre Nachrichtenformate generieren, bei denen die Feldreihenfolge im Protokollkopf strikt mit der Deklarationsreihenfolge in der Python Nachrichtenklassendefinition übereinstimmte. Eine Umordnung der Felder würde die Kompatibilität mit alten C++-Parsern auf der Börsenseite brechen, was zu Handelsablehnungen oder Systemabstürzen führen würde.
Lösung A: Manuelles Indizieren. Entwickler würden jedes Feld mit einer Sequenznummer wie field_order = 1 annotieren. Dieser Ansatz ist explizit und für Anfänger leicht zu verstehen. Er verletzt jedoch das DRY-Prinzip und wird zu einer Wartungsbelastung während der Refaktorisierung, da das Einfügen eines Feldes in die Mitte das Neunummerieren aller nachfolgenden Felder erfordert.
Lösung B: Quellcode-Parsing. Das Framework könnte das AST-Modul verwenden, um die Quellcode-Definition der Klasse zu parsen und die Reihenfolge der Zuweisungen zu extrahieren. Dies funktioniert ohne die Komplexität von Metaklassen. Leider scheitert es völlig, wenn Quellcodedateien zur Laufzeit nicht verfügbar sind, wie in gefrorenen binären Distributionen oder optimierten CPython-Bereitstellungen, die den Quellcode entfernen.
Lösung C: Metaklasse mit __prepare__. Durch die Rückgabe eines OrderedDict von __prepare__ erfasst die Metaklasse die natürliche Deklarationsreihenfolge automatisch. Dies ist robust in allen Bereitstellungsszenarien und transparent für Endbenutzer. Der einzige Nachteil ist die zusätzliche Komplexität des Verständnisses des Python-Metaklassenprotokolls, das ein senior-niveau Wissen erfordert.
Ausgewählte Lösung: Das Team wählte Lösung C, da sie Garantien zur Definitionzeit ohne Laufzeitüberhead pro Nachrichteninstanz bietet. Sie funktioniert zuverlässig in allen Bereitstellungsumgebungen, einschließlich solcher ohne Quellcode, und bewahrt die natürliche Klassensyntax, die Entwickler erwarten, während sie Einschränkungen in der frühestmöglichen Phase durchsetzt.
Ergebnis: Die Nachrichtenbibliothek hielt die drahtgebundene Formatkompatibilität automatisch aufrecht. Entwickler schrieben natürliche Klassendefinitionen, und das System generierte korrekte binäre Layouts. Vererbungshierarchien bewahrten korrekt die Reihenfolge der Felder der Eltern, bevor die Felder der Kinder, und lösten ein komplexes Problem in der Spezifikation des Handelsprotokolls ohne manuelles Eingreifen.
Frage 1: Warum muss __prepare__ als @staticmethod (oder @classmethod) und nicht als reguläre Instanzmethode definiert werden, und welcher Fehler tritt auf, wenn du diesen Dekorator weglässt?
Antwort: __prepare__ wird aufgerufen, bevor die Instanz der Metaklasse erstellt wird, was bedeutet, dass noch kein cls oder self zum Binden verfügbar ist. Python ruft __prepare__ auf, um den Namensraum zu generieren, der an __new__ übergeben wird. Wenn es als reguläre Instanzmethode definiert ist, die self erwartet, wird Python einen TypeError auslösen, der besagt, dass die Funktion positionsgebundene Argumente annimmt, aber keine gegeben wurden, da die Logik versucht, sie nur mit dem Namen, den Basen und den Schlüsselwort-Argumenten aufzurufen. Es muss eine statische Methode sein, um ohne implizite erste Argumentbindung aufgerufen zu werden, obwohl classmethod funktioniert, wenn du Zugriff auf die Metaklasse selbst benötigst.
Frage 2: Kann __prepare__ eine Zuordnung zurückgeben, die keine Unterklasse von dict ist, und welches spezifische Protokoll muss sie erfüllen, um während der Ausführung des Klassenkörpers korrekt zu funktionieren?
Antwort: Ja, es kann jede veränderbare Zuordnung zurückgeben, die das Protokoll der abstrakten Basisklasse MutableMapping implementiert, speziell mit den Anforderungen an __setitem__, __getitem__, __contains__ und idealerweise __iter__ oder keys() für die Umwandlung. Die Zuordnung muss jedoch nicht von dict erben. Die entscheidende Anforderung ist, dass es Zeichenfolgenkeys und beliebige Werte akzeptieren muss und sich wie ein Dictionary während der Attributzuweisungen im Klassenkörper verhält. Nach der Klassenausführung erhält die Metaklasse __new__ diese Zuordnung; wenn sie keine Unterklasse von dict ist, musst du sie explizit umwandeln (z. B. dict(namespace)), bevor du super().__new__ aufrufst, da das __dict__ des resultierenden Klassenobjekts ein Dictionary sein muss.
Frage 3: Wie behandelt __prepare__ die Schlüsselwortargumente, die im Kopf der Klassendefinition übergeben werden (z. B. class MyClass(metaclass=Meta, strict=True)), und was passiert, wenn diese nicht richtig weitergeleitet werden?
Antwort: Schlüsselwortargumente im Klassenkopf (über metaclass hinaus) werden an __prepare__ als **kwds übergeben. Wenn __prepare__ **kwargs (oder spezifische benannte Argumente) nicht akzeptiert, wird Python einen TypeError auslösen, der besagt, dass __prepare__ ein unerwartetes Schlüsselwortargument erhalten hat. Dies ist ein häufiges Problem, wenn Konfigurationsoptionen zu Metaklassen hinzugefügt werden. Die Methodensignatur muss __prepare__(name, bases, **kwargs) sein, um vorwärtskompatibel zu sein. Diese Schlüsselwörter werden auch anschließend an __new__ und __init__ weitergegeben, sodass die Metaklasse zur Vorbereitungszeit Konfiguration zur Anpassung des Verhaltens des Namensraums erhalten kann (z. B. die Wahl zwischen strikter und nachsichtiger Validierung).