Menu

Ereditarietà in Python: sottoclassi, super(), override e MRO

Come funziona l'ereditarietà tra classi in Python: creare una sottoclasse, chiamare super().__init__, ridefinire ed estendere metodi, isinstance e issubclass, ereditarietà multipla e MRO, e quando la composizione è la scelta migliore.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Una classe costruita su un'altra classe

L'ereditarietà ti permette di definire una nuova classe come estensione di una esistente. La classe esistente è il genitore (chiamato anche classe base o superclasse), quella nuova è la figlia (sottoclasse). La figlia ottiene ogni metodo e attributo di classe del genitore senza doverli ripetere, e può aggiungerne di propri o sostituire quelli che devono comportarsi diversamente.

Dichiari il genitore tra parentesi dopo il nome della classe:

Output:

Rex is an animal
Woof

Dog non ha né un __init__ né un describe propri. Quando chiami Dog("Rex"), Python cerca __init__ in Dog, non lo trova e usa quello di Animal. La stessa ricerca avviene per rex.describe(). Per rex.speak() trova prima speak in Dog e si ferma lì.

Tutto il meccanismo sta in quest'ordine di ricerca: Python cerca nella classe dell'oggetto, poi nel genitore, poi nel genitore del genitore, e usa la prima corrispondenza. Se le classi sono una novità per te, la pagina sulle classi spiega prima __init__, self e i metodi.

Ridefinire un metodo

Un metodo nella figlia con lo stesso nome di uno del genitore lo sostituisce per le istanze della figlia. Si chiama override. Ogni sottoclasse può rispondere alla stessa chiamata a modo suo:

Output:

Rectangle with area 12
Square with area 25
Shape with area 0

summary è definito una volta sola, in Shape, ma chiama self.area(). Visto che self è l'oggetto effettivo, un Rectangle esegue Rectangle.area e uno Square trova area nel suo genitore Rectangle. Il codice del genitore chiama il metodo della figlia senza sapere che la figlia esiste. È questo che rende utile l'ereditarietà per condividere un flusso di lavoro comune lasciando che ogni sottoclasse riempia i dettagli.

Chiamare il genitore con super()

Quando una figlia definisce un proprio __init__, il __init__ del genitore non viene più eseguito da solo. Se la figlia ha ancora bisogno della preparazione fatta dal genitore, chiamala con super().__init__():

Output:

Ada 1050.0

super() restituisce un oggetto che cerca i metodi partendo dal genitore della classe corrente. super().__init__(owner, balance) quindi esegue Account.__init__ sullo stesso oggetto, e dopo self.owner e self.balance esistono.

Se togli quella riga, l'oggetto è costruito solo a metà:

Output:

0.05
AttributeError: 'SavingsAccount' object has no attribute 'owner'

Puoi scrivere invece Account.__init__(self, owner), e il codice più vecchio lo fa spesso. super() è preferibile perché non ripete il nome del genitore e collabora correttamente con l'ereditarietà multipla, che vedremo più sotto.

Estendere un metodo invece di sostituirlo

super() funziona in qualsiasi metodo, non solo in __init__. Chiamare la versione del genitore dentro un override ti permette di aggiungere comportamento attorno a essa invece di riscriverla:

Output:

started
[12:00] started
INFO [12:00] started

Ogni livello aggiunge un pezzo e delega il resto verso l'alto. Se in futuro il format del genitore cambia, entrambe le figlie ereditano la modifica.

isinstance e issubclass

Un'istanza di una sottoclasse è anche un'istanza di ogni classe sopra di essa. isinstance(obj, cls) verifica questa relazione, e issubclass(child, parent) la verifica tra due classi:

Preferisci isinstance al confronto diretto di type(obj). Un controllo come type(obj) is Animal rifiuta ogni sottoclasse, e si rompe appena qualcuno aggiunge Dog.

Ogni classe che scrivi eredita da object, anche quando non lo dici. class Animal: equivale a class Animal(object):. Da lì arrivano i metodi predefiniti come __str__, __repr__ e __eq__, ed è per questo che ridefinire __str__ cambia quello che mostra print():

Output:

(2, 3)
True

Obbligare le sottoclassi a implementare un metodo

A volte il genitore non va mai usato direttamente, ma solo come modello. Il modulo abc contrassegna una classe del genere come astratta. Ogni metodo decorato con @abstractmethod deve essere ridefinito prima che la classe si possa istanziare:

Le prime tre righe dell'output sono l'esportazione:

exporting 2 rows
1,2
3,4

L'ultima riga è un TypeError che nomina Exporter e il suo metodo mancante export (la formulazione esatta cambia tra le versioni di Python). L'errore emerge quando l'oggetto viene creato, non più tardi quando finalmente viene chiamato export.

Ereditarietà multipla e MRO

Una classe può elencare più di un genitore. In quel caso eredita da tutti:

Output:

paddling flapping
swims
['Duck', 'Swimmer', 'Flyer', 'object']

Entrambi i genitori definiscono move, e Duck prende quello di Swimmer. Python decide con l'ordine di risoluzione dei metodi (MRO, method resolution order): un'unica lista di classi, salvata in Duck.__mro__, che ogni ricerca di attributi percorre dall'inizio alla fine. Parte dalla classe stessa, mantiene i genitori nell'ordine in cui li hai elencati, mette ogni classe prima dei propri genitori e finisce con object. Inverti l'ordine in class Duck(Flyer, Swimmer) e move restituisce "flies".

super() segue la stessa lista. Dentro un metodo di Swimmer, super() significa "la classe successiva a Swimmer nell'MRO della classe dell'oggetto", che per un Duck è Flyer, non object. È così che classi cooperative possono chiamare ciascuna super() e far eseguire ogni classe della catena esattamente una volta, anche in un diamante in cui due genitori condividono un nonno.

In pratica, l'ereditarietà multipla si usa soprattutto per i mixin: piccole classi che aggiungono una sola capacità, come un JsonMixin che aggiunge un metodo to_json, combinate con un genitore principale. Gerarchie profonde con diversi genitori completi sono difficili da seguire e servono raramente.

Ereditarietà o composizione?

L'ereditarietà modella una relazione "è un". Un SavingsAccount è un Account: qualsiasi codice che funziona con un Account dovrebbe funzionare con lui senza modifiche. Quando questa frase suona sbagliata, probabilmente ti serve la composizione, che modella "ha un": l'oggetto tiene un altro oggetto come attributo e lo chiama.

Output:

Roadster: 300 hp engine running

Anche class Car(Engine) funzionerebbe, ma sosterrebbe che un'auto è un tipo di motore, darebbe a Car ogni metodo del motore e renderebbe impossibile montare un motore diverso senza una classe diversa. Con la composizione le parti restano separate, e sostituire il motore significa solo passare un altro oggetto.

Qualche segnale che l'ereditarietà è lo strumento sbagliato:

  • La figlia ridefinisce la maggior parte dei metodi del genitore, o ne disattiva alcuni.
  • Erediti solo per riutilizzare uno o due metodi di supporto.
  • La gerarchia supera i due o tre livelli e devi leggere diversi file per capire cosa fa un metodo.

Errori comuni

  • Dimenticare super().__init__(). L'__init__ della figlia sostituisce quello del genitore, quindi gli attributi del genitore non vengono mai impostati e più avanti ottieni un AttributeError.
  • Passare self alle chiamate a super(). Scrivi super().__init__(name), non super().__init__(self, name). super() lega già il metodo all'oggetto corrente.
  • Firme incompatibili negli override. Se il speak(self) del genitore viene chiamato da codice condiviso, un speak(self, volume) della figlia con un argomento obbligatorio in più rompe quel codice. Mantieni gli override compatibili, o dai ai nuovi parametri dei valori predefiniti.
  • Controllare type(obj) == SomeClass. Ignora le sottoclassi. Usa isinstance.
  • Usare l'ereditarietà per condividere codice tra classi non collegate. Sposta piuttosto il codice condiviso in una funzione o in un oggetto di supporto.

Domande frequenti

Come si eredita da una classe in Python?

Metti la classe genitore tra parentesi dopo il nome della nuova classe: class Dog(Animal):. Dog ottiene ogni metodo e attributo di classe di Animal, e può aggiungerne di nuovi o ridefinire quelli esistenti.

Cosa fa super().__init__()?

Chiama l'__init__ della classe genitore, così il genitore può impostare i propri attributi. Quando una sottoclasse definisce un proprio __init__, Python non chiama in automatico quello del genitore, quindi chiami super().__init__(...) con gli argomenti che servono al genitore e poi imposti gli attributi in più della sottoclasse.

Qual è la differenza tra isinstance e type in Python?

isinstance(obj, Animal) è True per un Animal e per le istanze di qualsiasi sottoclasse di Animal. type(obj) is Animal è True solo quando l'oggetto è stato creato proprio da Animal. Preferisci isinstance, perché continua a funzionare quando qualcuno aggiunge una sottoclasse.

Python supporta l'ereditarietà multipla?

Sì: class C(A, B): eredita da entrambe. Quando entrambi i genitori definiscono lo stesso metodo, Python ne sceglie uno seguendo l'ordine di risoluzione dei metodi (MRO), che puoi esaminare con C.__mro__. Elenca prima C, poi i suoi genitori da sinistra a destra e per ultimo object.

Quando usare la composizione invece dell'ereditarietà?

Usa l'ereditarietà per una relazione "è un", in cui la sottoclasse può essere usata ovunque ci si aspetti il genitore (un SavingsAccount è un Account). Usa la composizione, cioè tenere un oggetto come attributo, per una relazione "ha un" (una Car ha un Engine). Nel dubbio, la composizione è più facile da cambiare in seguito.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA