PythonProgrammierungSenior Python Entwickler

Wie löst **Python**'s null-Argument `super()`, die definierende Klasse korrekt, wenn die Methode von mehreren Unterklassen geerbt wird?

Bestehen Sie Vorstellungsgespräche mit dem Hintsage-KI-Assistenten

Antwort auf die Frage

Das null-Argument super() basiert auf einer vom Compiler generierten Closure-Zelle namens __class__, die implizit für jede Methode erstellt wird, die lexikalisch innerhalb eines Python-Klassenkörpers definiert ist. Wenn der Compiler eine Klassendefinition verarbeitet, erstellt er eine Zellvariable __class__ in der Closure der Methode, die auf das derzeit definierte Klassenobjekt verweist. Wenn super() ohne Argumente aufgerufen wird, inspiziert die C-Implementierung das aufrufende Frame, lokalisiert diese __class__-Zelle und verwendet sie als das erste Argument (den Typ). Anschließend wird das erste Positionsargument der Methode (in der Regel self) als Instanz verwendet. Dieser Mechanismus bindet die Klassenreferenz zur Zeit der Definition und nicht zur Zeit des Aufrufs, wodurch die Notwendigkeit entfällt, Klassennamen fest einzugeben, während sichergestellt wird, dass jede Methode in einer Vererbungskette auf ihre spezifische Position in der MRO (Methodenauflösungsreihenfolge) verweist.

class Base: def method(self): return "Base" class Middle(Base): def method(self): # __class__ ist hier an Middle gebunden return f"Middle -> {super().method()}" class Derived(Middle): def method(self): # __class__ ist hier an Derived gebunden return f"Derived -> {super().method()}"

Lebenssituation

Wir haben eine quantitative Handelsbibliothek mit einer tiefen Hierarchie von Preisgestaltungsmodellen gewartet. Das BaseModel stellte eine Methode calculate_risk() bereit, EquityModel hat sie überschrieben, um aktienspezifische Logik hinzuzufügen, und AmericanOptionModel spezialisierte sie weiter. Während einer umfangreichen Refaktorisierung, um EquityModel in VanillaEquityModel umzubenennen, entdeckten wir Dutzende von veralteten super(EquityModel, self)-Aufrufen in Mixinklassen, die kopiert und eingefügt worden waren. Diese veralteten Referenzen verursachten TypeError oder stille logische Fehler, bei denen die falsche Elternmethode aufgerufen wurde, was die Risikoabschätzungen in der Produktion beeinträchtigte.

Lösung 1: Globale Suche-und-Ersetzen-Refaktorisierung. Wir haben in Betracht gezogen, automatisierte Werkzeuge zu verwenden, um alle hartkodierten Klassennamen in super()-Aufrufen im gesamten 200.000-Zeilen-Code zu finden und zu ersetzen. Vorteile: Es erfordert keine architektonischen Änderungen und funktioniert mit dem Legacy-Python 2-Syntax. Nachteile: Es ist fragil und unvollständig; es werden dynamisch generierte Klassen, stringbasierte dynamische Methoden-Zuweisungen und Referenzen in Drittanbietererweiterungen übersehen. Es verstößt auch gegen das DRY-Prinzip, da der Klassenname in jeder Methode wiederholt wird.

Lösung 2: Universelle Einführung von null-Argument super(). Wir haben den gesamten Code grundlegend umgestellt, um super() ohne Argumente zu verwenden. Vorteile: Dadurch wird das Umbenennen von Klassen völlig sicher, es beseitigt eine große Quelle menschlicher Fehler während der Refaktorisierung und verbessert die Lesbarkeit erheblich, indem redundanter Lärm entfernt wird. Es behandelt komplexe kooperative Mehrfachvererbungsmuster korrekt. Nachteile: Es erfordert Python 3.6+ (das wir hatten), und Entwickler, die mit dem impliziten Closurmechanismus nicht vertraut waren, fanden es anfangs verwirrend. Es kann auch nicht in Funktionen verwendet werden, die dynamisch nach der Definition an Klassen angehängt werden.

Lösung 3: Metaklasseninjektion von Klassenreferenzen. Wir haben kurz überlegt, eine Metaklasse zu verwenden, um ein Attribut _defining_class in jede Methode einzufügen. Vorteile: Dies macht den Mechanismus explizit und inspizierbar. Nachteile: Es fügt erhebliche Komplexität und Overhead hinzu, steht im Widerspruch zur Standard-CPython-Optimierung und erfindet ein bereits vom Sprachcompiler bereitgestelltes Feature neu.

Wir haben uns für Lösung 2 entschieden. Die Migration wurde in einem Sprint abgeschlossen. Das Ergebnis war eine Reduzierung der Zeit um 40% bei nachfolgenden Refaktorisierungsaufgaben, die das Umbenennen von Klassen betrafen, und die Beseitigung einer ganzen Klasse von Fehlern im Zusammenhang mit veralteten super()-Referenzen in unserer CI-Pipeline.

Was Kandidaten oft übersehen

Wie lokalisiert super() physisch die __class__-Zelle, wenn es mit null Argumenten aufgerufen wird?

Die Implementierung von super() in CPython (in Objects/typeobject.c) verwendet PyEval_GetLocals(), um die lokalen Variablen und die Closure des aufrufenden Frames zu inspizieren. Es sucht speziell nach einer freien Variablen (Zelle) namens __class__. Diese Zelle wird nur vom Compiler erstellt, wenn ein Funktionskörper lexikalisch innerhalb eines Klassenkörpers definiert wird (angegeben durch das CO_OPTIMIZED-Flag und den Klassenscope). Wenn die Zelle gefunden wird, extrahiert super() das Klassenobjekt; wenn nicht, wirft es RuntimeError: super(): __class__-Zelle nicht gefunden. Die Null-Argument-Form wird im Wesentlichen vom Compiler in super(__class__, self) umgewandelt, wobei __class__ die geschlossene Variable ist.

Was passiert, wenn Sie versuchen, null-Argument super() innerhalb einer Funktion zu verwenden, die nach der Erstellung der Klasse als Klassenattribut zugewiesen wird?

Wenn Sie eine Funktion außerhalb eines Klassenkörpers definieren und sie dann als Methode zuweisen (z.B. MyClass.method = some_function), wird der Aufruf von super() innerhalb dieser Funktion einen RuntimeError auslösen. Dies geschieht, weil der Compiler die __class__-Zelle nur für Codeobjekte erstellt, die als Teil eines Klassenprüfungs gesetzt wurden. Ohne die Zelle hat super() keine Möglichkeit zu bestimmen, welche Klasse in der Hierarchie die "aktuelle" Klasse ist, da es nicht zwischen dem Funktionsdefinitionsbereich und der Klasse, an die sie später angehängt wurde, unterscheiden kann.

Warum verursacht null-Argument super() keine unendliche Rekursion, wenn eine Unterklassenmethode super() aufruft und die Elternmethode ebenfalls super() aufruft?

Dies funktioniert, weil __class__ auf die Klasse verweist, in der die Methode definiert wurde, nicht auf die zur Laufzeit Klasse der Instanz (type(self)). Wenn Derived.method() super() aufruft, stellt es fest, dass __class__ Derived ist und an die nächste Klasse in Derived.__mro__ delegiert (z.B. Middle). Wenn Middle.method() erreicht wird und es super() aufruft, enthält seine eigene distincte __class__-Zelle Middle, also untersucht es die nächste Klasse nach Middle (z.B. Base). Jede Ebene der Hierarchie verwendet ihre eigene Klassenausweisungsreferenz zur Zeit der Definition, um sicherzustellen, dass die MRO linear nach oben durchsucht wird, ohne einmal zurück zur Unterklasse zu gelangen.