Bezargumentowy super() polega na zamknięciu kompilatora o nazwie __class__, które jest domyślnie tworzone dla każdej metody zdefiniowanej leksykalnie w ciele klasy Python. Gdy kompilator przetwarza definicję klasy, tworzy zmienną komórkową __class__ w zamknięciu metody, która wskazuje na obiekt klasy, który jest obecnie definiowany. Kiedy super() jest wywoływane bez argumentów, implementacja C sprawdza ramkę wywołania, lokalizuje tę komórkę __class__ i używa jej jako pierwszego argumentu (typ). Następnie używa pierwszego argumentu pozycyjnego metody (zwykle self) jako instancji. Ten mechanizm wiąże odniesienie do klasy w czasie definiowania, a nie wywołania, eliminując potrzebę twardego kodowania nazw klas, jednocześnie zapewniając, że każda metoda w łańcuchu dziedziczenia odnosi się do swojej własnej specyficznej pozycji w MRO (Kolejności Rozwiązywania Metod).
class Base: def method(self): return "Base" class Middle(Base): def method(self): # __class__ jest powiązane z Middle tutaj return f"Middle -> {super().method()}" class Derived(Middle): def method(self): # __class__ jest powiązane z Derived tutaj return f"Derived -> {super().method()}"
Utrzymywaliśmy bibliotekę handlu ilościowego z głęboką hierarchią modeli wyceny. BaseModel dostarczało metodę calculate_risk(), EquityModel nadpisywało ją, aby dodać logikę specyficzną dla akcji, a AmericanOptionModel dalej ją specjalizowało. Podczas dużego refaktoryzacji, aby zmienić nazwę EquityModel na VanillaEquityModel, odkryliśmy dziesiątki przestarzałych odniesień super(EquityModel, self) w klasach mixin, które były kopiowane i wklejane. Te przestarzałe odniesienia powodowały TypeError lub ciche błędy logiczne, gdy wywoływana była niewłaściwa metoda rodzica, przerywając obliczenia ryzyka produkcyjnego.
Rozwiązanie 1: Globalne refaktoryzacje wyszukiwania i zamiany. Rozważaliśmy użycie narzędzi automatycznych do znalezienia i zastąpienia wszystkich twardo zakodowanych nazw klas w wywołaniach super() w kodzie o objętości 200 000 linii. Zalety: Nie wymaga zmian architektonicznych i działa z legacy Python 2. Wady: Jest kruchy i niekompletny; pomija dynamicznie generowane klasy, przypisania metody oparte na stringach oraz odniesienia w rozszerzeniach innych firm. Narusza również zasadę DRY, ponieważ nazwa klasy powtarza się w każdej metodzie.
Rozwiązanie 2: Uniwersalne wprowadzenie zero-argumentowego super(). Przenieśliśmy całą bazę kodu do używania super() bez argumentów. Zalety: To sprawia, że zmiana nazw klas jest całkowicie bezpieczna, eliminuje główne źródło błędów ludzkich podczas refaktoryzacji i znacząco poprawia czytelność poprzez usunięcie zbędnego hałasu. Prawidłowo obsługuje złożone wzorce współpracy w dziedziczeniu wielokrotnym. Wady: Wymaga Pythona 3.6+ (którego posiadaliśmy), a deweloperzy nieznający mechanizmu zamknięć na początku uznawali go za mylący. Nie można go również używać w funkcjach dynamicznie przypisywanych do klas po ich zdefiniowaniu.
Rozwiązanie 3: Wstrzykiwanie odniesień klas za pomocą metaklasy. Krótko rozważaliśmy użycie metaklasy do wstrzyknięcia atrybutu _defining_class do każdej metody. Zalety: To czyni mechanizm explicytnym i możliwym do zbadania. Wady: To dodaje znaczną złożoność i przeciążenie, koliduje z optymalizacją standardowego CPython oraz wymyśla funkcję, którą już zapewnia kompilator języka.
Wybraliśmy Rozwiązanie 2. Migracja została zakończona w trakcie jednej sprintu. Rezultatem była 40% redukcja czasu spędzonego na kolejnych zadaniach refaktoryzacyjnych związanych z zmianą nazw klas oraz eliminacja całej klasy błędów związanych z przestarzałymi referencjami super() w naszym pipeline CI.
Jak super() fizycznie lokalizuje komórkę __class__, gdy jest wywoływane z zerową liczbą argumentów?
Implementacja super() w CPython (w Objects/typeobject.c) używa PyEval_GetLocals(), aby zbadać lokalne zmienne i zamknięcie ramki wywołania. W szczególności wyszukuje zmienną wolną (komórkę) o nazwie __class__. Ta komórka jest tworzona przez kompilator tylko wtedy, gdy funkcja jest zdefiniowana leksykalnie w ciele klasy (co jest wskazywane przez flagę CO_OPTIMIZED i zakres klasy). Jeśli komórka zostanie znaleziona, super() wyodrębnia obiekt klasy; jeśli nie, podnosi RuntimeError: super(): __class__ cell not found. Formę bezargumentową kompilator zasadniczo przekształca w super(__class__, self), gdzie __class__ jest zmienną zamkniętą.
Co się stanie, jeśli spróbujesz użyć zerowargowego super() wewnątrz funkcji, która jest przypisywana do atrybutu klasy po utworzeniu klasy?
Jeśli zdefiniujesz funkcję poza ciałem klasy i następnie przypiszesz ją jako metodę (np. MyClass.method = some_function), wywołanie super() wewnątrz tej funkcji spowoduje wzrost RuntimeError. Dzieje się tak, ponieważ kompilator tworzy komórkę __class__ tylko dla obiektów kodu skompilowanych jako część suite klasy. Bez komórki super() nie ma sposobu, aby określić, która klasa w hierarchii jest "obecna", ponieważ nie może odróżnić zakresu definicji funkcji od klasy, do której została później przypisana.
Dlaczego zerowy argument super() nie powoduje nieskończonej rekurencji, gdy metoda klasy podrzędnej wywołuje super(), a metoda rodzica również wywołuje super()?
Działa to, ponieważ __class__ odnosi się do klasy, w której metoda jest zdefiniowana, a nie do klasy wykonawczej instancji (type(self)). Gdy Derived.method() wywołuje super(), znajduje, że __class__ to Derived i deleguje do następnej klasy w Derived.__mro__ (np. Middle). Kiedy dotrze do Middle.method(), które wywołuje super(), jego własna wyraźna komórka __class__ zawiera Middle, więc odszukuje następną klasę po Middle (np. Base). Każdy poziom hierarchii używa swojego własnego odniesienia do klasy w czasie definicji, zapewniając, że MRO jest przeszukiwane liniowo w górę, dokładnie raz, bez powracania do klasy podrzędnej.