Metoda __prepare__ została wprowadzona w Python 3.0 za pomocą PEP 3115, aby rozwiązać podstawowe ograniczenia w protokole tworzenia klas. Przed tą zmianą przestrzeń nazw używana podczas wykonywania ciała klasy była zawsze standardowym słownikiem, co uniemożliwiało zachowanie porządku deklaracji atrybutów lub przechwytywanie przypisań w momencie ich występowania. Stało się to szczególnie problematyczne dla deweloperów budujących ORM oraz biblioteki serializacyjne, które musiały śledzić sekwencję deklaracji pól bez uciekania się do kruchych analiz kodu źródłowego.
Kiedy Python wykonuje ciało klasy, popularyzuje mapowanie przestrzeni nazw, które ostatecznie stanie się __dict__ klasy. Domyślny typ dict nie gwarantuje porządku wstawiania w starszych wersjach Python i brakuje mu haków do walidacji lub transformacji nazw w momencie ich definiowania. Deweloperzy wymagający ograniczeń czasowych deklaracji — takich jak zakazywanie pewnych wzorców nazewnictwa lub śledzenie kolejności pól dla protokołów binarnych — nie mieli czystego mechanizmu do podłączenia się w tej konkretnej fazie konstrukcji klasy przed sfinalizowaniem obiektu klasy.
Implementując __prepare__ jako metodę statyczną w metaklasie, możesz zwrócić niestandardowe mutowalne mapowanie (takie jak collections.OrderedDict lub niestandardowy słownik walidujący), które będzie służyć jako przestrzeń nazw. To mapowanie przechwytyuje wszystkie przypisania na poziomie klasy podczas wykonywania ciała, umożliwiając wstępne przetwarzanie przed sfinalizowaniem klasy przez metodę __new__ metaklasy. Niestandardowa przestrzeń nazw jest następnie przekazywana do __new__, gdzie może być przekształcona w standardowy dict lub zachowana dla uporządkowanego dostępu.
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']
Platforma handlowa musiała generować binarne formaty wiadomości, gdzie kolejność pól w nagłówku protokołu ściśle odpowiadała kolejności deklaracji w definicji klasy wiadomości Python. Zmiana kolejności pól mogłaby złamać zgodność z legacy parserami C++ po stronie wymiany, prowadząc do odrzucenia transakcji lub awarii systemu.
Rozwiązanie A: Ręczne indeksowanie. Deweloperzy oznaczaliby każde pole numerem sekwencyjnym, na przykład field_order = 1. To podejście jest jasne i łatwe do zrozumienia dla początkujących. Niemniej jednak narusza zasadę DRY i staje się obciążeniem w utrzymaniu podczas refaktoryzacji, ponieważ wstawienie pola w środku wymaga ponownego numerowania wszystkich kolejnych pól.
Rozwiązanie B: Analiza kodu źródłowego. Ramy mogłyby użyć modułu AST do analizy źródła definicji klasy i wyodrębnienia kolejności przypisań. To działa bez złożoności metaklasy. Niestety całkowicie zawodzę, gdy pliki źródłowe nie są dostępne w czasie wykonywania, takie jak w zamrożonych dystrybucjach binarnych lub optymalizowanych wdrożeniach CPython, które usuwają kod źródłowy.
Rozwiązanie C: Metaklasa z __prepare__. Zwracając OrderedDict z __prepare__, metaklasa automatycznie przechwytuje naturalny porządek deklaracji. To jest solidne we wszystkich scenariuszach wdrożeniowych i przejrzyste dla użytkowników końcowych. Jedynym minusem jest dodatkowa złożoność zrozumienia protokołu metaklasy Python, co wymaga wiedzy na poziomie seniora.
Wybrane rozwiązanie: Zespół wybrał rozwiązanie C, ponieważ zapewnia gwarancje w czasie definiowania bez narzutu w czasie wykonywania dla każdej instancji wiadomości. Działa niezawodnie we wszystkich środowiskach wdrożeniowych, w tym w tych bez kodu źródłowego, i zachowuje naturalną składnię klasy, której oczekują deweloperzy, jednocześnie egzekwując ograniczenia na jak najwcześniejszym etapie.
Rezultat: Biblioteka wiadomości automatycznie zachowała zgodność z formatem. Deweloperzy pisali naturalne definicje klas, a system generował poprawne układy binarne. Hierarchie dziedziczenia prawidłowo zachowały kolejność pól rodziców przed polami dzieci, rozwiązując złożony problem w specyfikacji protokołu handlowego bez interwencji ręcznej.
Pytanie 1: Dlaczego __prepare__ musi być definiowane jako @staticmethod (lub @classmethod), a co się stanie, jeśli pominiesz ten dekorator?
Odpowiedź: __prepare__ jest wywoływany przed utworzeniem instancji metaklasy, co oznacza, że nie ma dostępnego cls lub self. Python wywołuje __prepare__, aby wygenerować przestrzeń nazw, która będzie przekazywana do __new__. Jeśli zdefiniujesz to jako zwykłą metodę instancyjną oczekującą self, Python zgłosi błąd TypeError, wskazując, że funkcja przyjmuje argumenty pozycyjne, ale żadne nie zostały podane, ponieważ mechanizm próbuje wywołać ją tylko z nazwą, bazami i słowami kluczowymi. Musi to być metoda statyczna, aby mogła być wywołana bez pośredniego podwiązywania pierwszego argumentu, chociaż classmethod działa, jeśli potrzebujesz dostępu do samej metaklasy.
Pytanie 2: Czy __prepare__ może zwrócić mapowanie, które nie jest podklasą dict, i jaki konkretny protokół musi spełniać, aby działać poprawnie podczas wykonywania ciała klasy?
Odpowiedź: Tak, może zwrócić każdą mutowalną mapę implementującą protokół abstrakcyjnej bazy klasowej MutableMapping, wymagającą konkretnego przywrócenia: __setitem__, __getitem__, __contains__, a idealnie __iter__ lub keys() dla konwersji. Jednak mapowanie nie musi dziedziczyć z dict. Kluczowym wymogiem jest to, że musi akceptować klucze typu string i dowolne wartości, zachowując się jak słownik podczas przypisywania atrybutów w ciele klasy. Po wykonaniu klasy metaklasa __new__ otrzymuje to mapowanie; jeżeli nie jest to podklasa dict, musisz jawnie przekształcić je (np. dict(namespace)) przed wywołaniem super().__new__, ponieważ __dict__ obiektu klasy musi być słownikiem.
Pytanie 3: Jak __prepare__ obsługuje argumenty słownikowe przekazywane w nagłówku definicji klasy (np. class MyClass(metaclass=Meta, strict=True)), a co się stanie, jeśli nie zostaną one prawidłowo przekazane?
Odpowiedź: Argumenty słownikowe w nagłówku klasy (oprócz metaclass) są przekazywane do __prepare__ jako **kwds. Jeśli __prepare__ nie akceptuje **kwargs (lub konkretnych nazwanych argumentów), Python zgłosi błąd TypeError, mówiąc, że __prepare__ otrzymał nieoczekiwany argument słownikowy. To powszechny problem, gdy dodajesz opcje konfiguracyjne do metaklas. Podpis metody musi być __prepare__(name, bases, **kwargs), aby był zgodny w przyszłości. Te słowa kluczowe są również następnie przekazywane do __new__ i __init__, co pozwala metaklasie otrzymać konfigurację w czasie przygotowania, aby dostosować zachowanie przestrzeni nazw (np. wybierając między trybami walidacji ścisłej i łagodnej).