Il super() senza argomenti si basa su una cella di chiusura generata dal compilatore chiamata __class__ che viene implicitamente creata per ogni metodo definito lessicalmente all'interno di un corpo di classe in Python. Quando il compilatore elabora una definizione di classe, crea una variabile cella __class__ nella chiusura del metodo che punta all'oggetto classe attualmente in fase di definizione. Quando super() viene chiamato senza argomenti, l'implementazione in C ispeziona il frame di chiamata, localizza questa cella __class__ e la utilizza come primo argomento (il tipo). Quindi utilizza il primo argomento posizionale del metodo (di solito self) come istanza. Questo meccanismo lega il riferimento della classe al momento della definizione piuttosto che al momento della chiamata, eliminando la necessità di codificare nomi di classe mentre si assicura che ogni metodo in una catena di ereditarietà faccia riferimento alla propria specifica posizione nell'MRO (Ordinamento di Risoluzione dei Metodi).
class Base: def method(self): return "Base" class Middle(Base): def method(self): # __class__ è legato a Middle qui return f"Middle -> {super().method()}" class Derived(Middle): def method(self): # __class__ è legato a Derived qui return f"Derived -> {super().method()}"
Abbiamo mantenuto una libreria di trading quantitativo con una profonda gerarchia di modelli di pricing. Il BaseModel forniva un metodo calculate_risk(), EquityModel lo sovrascriveva per aggiungere logica specifica delle azioni e AmericanOptionModel lo specializzava ulteriormente. Durante una grande ristrutturazione per rinominare EquityModel in VanillaEquityModel, abbiamo scoperto dozzine di vecchi riferimenti a super(EquityModel, self) nelle classi mixin che erano stati copiati e incollati. Questi riferimenti obsoleti causavano errori TypeError o errori logici silenziosi in cui veniva invocato il metodo genitore sbagliato, interrompendo i calcoli del rischio in produzione.
Soluzione 1: Refactoring globale cerca-e-sostituisci. Abbiamo preso in considerazione l'uso di strumenti automatizzati per trovare e sostituire tutti i nomi di classe codificati nel codice in chiamate a super() nell'intero codice di 200.000 righe. Pro: Non richiede modifiche architettoniche e funziona con la sintassi legacy di Python 2. Contro: È fragile e incompleta; manca classi generate dinamicamente, assegnazioni di metodi dinamici basate su stringhe e riferimenti in estensioni di terze parti. Inoltre viola il principio DRY, poiché il nome della classe è ripetuto in ogni metodo.
Soluzione 2: Adozione universale di super() senza argomenti. Abbiamo migrato l'intero codice per utilizzare super() senza argomenti. Pro: Questo rende il rinominamento delle classi completamente sicuro, elimina una grande fonte di errore umano durante il refactoring e migliora significativamente la leggibilità rimuovendo rumore ridondante. Gestisce correttamente modelli complessi di ereditarietà multipla cooperativa. Contro: Richiede Python 3.6+ (che avevamo), e gli sviluppatori non familiari con il meccanismo di chiusura implicita inizialmente lo trovavano confuso. Inoltre, non può essere utilizzato in funzioni assegnate dinamicamente a classi dopo la definizione.
Soluzione 3: Iniezione di riferimenti di classe nella metaclasse. Abbiamo brevemente considerato di utilizzare una metaclasse per iniettare un attributo _defining_class in ogni metodo. Pro: Questo rende il meccanismo esplicito e ispezionabile. Contro: Aggiunge una complessità e un overhead significativi, confligge con l'ottimizzazione standard di CPython e reinventa una funzionalità già fornita dal compilatore del linguaggio.
Abbiamo scelto la Soluzione 2. La migrazione è stata completata in un sprint. Il risultato è stato una riduzione del 40% del tempo trascorso in successivi compiti di refactoring che comportavano rinominare classi, e l'eliminazione di un'intera categoria di bug legati a riferimenti obsoleti a super() nella nostra pipeline CI.
Come trova fisicamente super() la cella __class__ quando viene chiamato senza argomenti?
L'implementazione di super() in CPython (in Objects/typeobject.c) utilizza PyEval_GetLocals() per ispezionare le variabili locali e la chiusura del frame di chiamata. Cerca specificamente una variabile libera (cella) chiamata __class__. Questa cella viene creata dal compilatore solo quando una funzione è definita lessicalmente all'interno di un corpo di classe (indicata dal flag CO_OPTIMIZED e dallo scopo della classe). Se la cella viene trovata, super() estrae l'oggetto classe; in caso contrario, solleva RuntimeError: super(): cella __class__ non trovata. La forma senza argomenti viene essenzialmente trasformata dal compilatore in super(__class__, self), dove __class__ è la variabile chiusa.
Cosa succede se provi a utilizzare super() senza argomenti all'interno di una funzione che è assegnata a un attributo di classe dopo che la classe è stata creata?
Se definisci una funzione al di fuori di un corpo di classe e poi la assegni come metodo (ad esempio, MyClass.method = some_function), chiamando super() all'interno di quella funzione genererai un RuntimeError. Questo si verifica perché il compilatore crea la cella __class__ solo per gli oggetti codice compilati come parte di una suite di classi. Senza la cella, super() non ha modo di determinare quale classe nella gerarchia è la "classe corrente", poiché non può distinguere tra l'ambito di definizione della funzione e la classe a cui è stata successivamente allegata.
Perché super() senza argomenti non causa ricorsione infinita quando un metodo della sottoclasse chiama super() e il metodo genitore chiama anch'esso super()?
Questo funziona perché __class__ si riferisce alla classe in cui il metodo è definito, non alla classe di runtime dell'istanza (type(self)). Quando Derived.method() chiama super(), trova che __class__ è Derived e delega alla classe successiva in Derived.__mro__ (ad esempio, Middle). Quando viene raggiunto Middle.method() e chiama super(), la propria cella __class__ distintiva contiene Middle, quindi cerca la classe successiva dopo Middle (ad esempio, Base). Ogni livello della gerarchia utilizza il proprio riferimento alla classe al momento della definizione, assicurando che l'MRO venga attraversato linearmente verso l'alto esattamente una volta senza tornare indietro alla sottoclasse.