De methode __prepare__ werd geïntroduceerd in Python 3.0 via PEP 3115 om fundamentele beperkingen in het klassecreatieprotocol aan te pakken. Voor deze wijziging was de namespace die tijdens de uitvoering van de klassebody werd gebruikt altijd een standaardwoordenboek, wat geen manier bood om de volgorde van attribuutdeclaraties te behouden of toewijzingen te onderscheppen zoals ze gebeurden. Dit werd bijzonder problematisch voor ontwikkelaars die ORM-s en serialisatiebibliotheken bouwden die de volgorde van velddeclaraties moesten volgen zonder te vertrouwen op fragiele broncodeanalyse.
Wanneer Python een klassebody uitvoert, vult het een namespace-mapping die uiteindelijk de klasse __dict__ zal worden. Het standaard dict type garandeert in oudere Python versies geen invoervolgorde en mist haakjes om namen te valideren of te transformeren op het moment dat ze worden gedefinieerd. Ontwikkelaars die beperkingen op het moment van declaratie vereisen—zoals het verbieden van bepaalde naamgevingspatronen of het volgen van veldvolgorde voor binaire protocollen—hadden geen schone manier om in deze specifieke fase van de klassenconstructie in te haken voordat het klasseobject werd gefinaliseerd.
Door __prepare__ als een statische methode in een metaklasse te implementeren, kunt u een aangepaste mutabele mapping (zoals collections.OrderedDict of een aangepaste validerende woordenboek) retourneren om als de namespace te fungeren. Deze mapping legt alle klasse-niveau toewijzingen vast tijdens de body-uitvoering, zodat voorbewerking mogelijk is voordat de metaklasse __new__-methode de klasse finalizeert. De aangepaste namespace wordt vervolgens doorgegeven aan __new__, waar deze kan worden omgezet naar een standaard dict of bewaard kan blijven voor geordende toegang.
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']
Een financieel handelsplatform moest binaire berichtformaten genereren waarbij de veldvolgorde in de protocolheader strikt overeenkwam met de declaratievolgorde in de Python berichtklasse-definitie. Het opnieuw ordenen van velden zou de compatibiliteit met legacy C++ parsers aan de uitwisselingzijde breken, wat leidde tot handelsweigerings of systeemcrashes.
Oplossing A: Handmatige indexering. Ontwikkelaars zouden elk veld annoteren met een volgnummer zoals field_order = 1. Deze aanpak is expliciet en gemakkelijk te begrijpen voor beginners. Het schendt echter het DRY-principe en wordt een onderhoudsprobleem tijdens refactoring, omdat het invoegen van een veld in het midden vereist dat alle volgende velden opnieuw worden genummerd.
Oplossing B: Broncode-analyse. Het kader zou de AST-module kunnen gebruiken om de bron van de klasse-definitie te parseren en de toewijzingsvolgorde te extraheren. Dit werkt zonder de complexiteit van metaklassen. Helaas faalt het volledig wanneer bronbestanden niet beschikbaar zijn tijdens runtime, zoals in bevroren binaire distributies of geoptimaliseerde CPython-implementaties die de broncode strippen.
Oplossing C: Metaklasse met __prepare__. Door een OrderedDict van __prepare__ te retourneren, legt de metaklasse automatisch de natuurlijke declaratievolgorde vast. Dit is robuust in alle implementatiescenario's en transparant voor eindgebruikers. Het enige nadeel is de extra complexiteit van het begrijpen van Python's metaklasseprotocol, dat senior-level kennis vereist.
Gekozen oplossing: Het team koos Oplossing C omdat het garanties geeft op het moment van definitie zonder runtime overhead per berichtinstantie. Het werkt betrouwbaar in alle implementatieomgevingen, inclusief die zonder broncode, en behoudt de natuurlijke klasse-syntax die ontwikkelaars verwachten terwijl het beperkingen afdwingt op het vroegst mogelijke moment.
Resultaat: De berichtenbibliotheek handhaafde automatisch de wire-format compatibiliteit. Ontwikkelaars schreven natuurlijke klasse-definities, en het systeem genereerde correcte binaire indelingen. Overervingshiërarchieën bewaarden correct de volgorde van oudervelden vóór kindvelden, waardoor een complex probleem in de specificatie van het handelsprotocol zonder handmatige tussenkomst werd opgelost.
Vraag 1: Waarom moet __prepare__ worden gedefinieerd als een @staticmethod (of @classmethod) in plaats van een reguliere instantie-methode, en welke fout treedt op als u deze decorator weglaat?
Antwoord: __prepare__ wordt aangeroepen voordat de metaklasse-instantie is gemaakt, wat betekent dat er nog geen cls of self beschikbaar is om aan te binden. Python roept __prepare__ aan om de namespace te genereren die aan __new__ zal worden doorgegeven. Als het wordt gedefinieerd als een reguliere instantie-methode die self verwacht, zal Python een TypeError genereren die aangeeft dat de functie positieargumenten vereist, maar er geen zijn gegeven, omdat de machine probeert het aan te roepen met alleen de naam, bases, en trefwoordenargumenten. Het moet een statische methode zijn om zonder impliciete binding van het eerste argument te worden aangeroepen, hoewel classmethod werkt als u toegang tot de metaklasse zelf nodig heeft.
Vraag 2: Kan __prepare__ een mapping retourneren die geen subklasse van dict is, en welk specifiek protocol moet het voldoen om correct te functioneren tijdens de uitvoering van de klassebody?
Antwoord: Ja, het kan elke mutabele mapping retourneren die het MutableMapping abstracte basisclass-protocol implementeert, specifiek vereisend __setitem__, __getitem__, __contains__, en idealiter __iter__ of keys() voor conversie. De mapping hoeft echter niet van dict te erven. De kritische vereiste is dat het string-sleutels en willekeurige waarden moet accepteren, en zich als een woordenboek moet gedragen tijdens de attribuuttoewijzing in de klassebody. Na de uitvoering van de klasse ontvangt de metaklasse __new__ deze mapping; als het geen subklasse van dict is, moet u deze expliciet converteren (bijv. dict(namespace)) voordat u super().__new__ aanroept, omdat het __dict__ van het resulterende klasseobject een woordenboek moet zijn.
Vraag 3: Hoe gaat __prepare__ om met trefwoordargumenten die zijn doorgegeven in de kop van de klasse-definitie (bijv. class MyClass(metaclass=Meta, strict=True)), en wat gebeurt er als deze niet correct worden doorgestuurd?
Antwoord: Trefwoordargumenten in de klassekop (opnieuw buiten metaclass) worden als **kwds aan __prepare__ doorgegeven. Als __prepare__ **kwargs (of specifieke benoemde argumenten) niet accepteert, zal Python een TypeError genereren met de melding dat __prepare__ een onverwacht trefwoordargument kreeg. Dit is een veel voorkomende valkuil bij het toevoegen van configuratieopties aan metaklassen. De methode-handtekening moet __prepare__(name, bases, **kwargs) zijn om vooruitcompatibel te zijn. Deze trefwoorden worden ook vervolgens doorgegeven aan __new__ en __init__, waardoor de metaklasse configuratie op voorbereidende tijd kan ontvangen om het gedrag van de namespace aan te passen (bijv. kiezen tussen strikte en soepele validatiemodi).