Ereditarietà in Python
Impara l'ereditarietà Python: sottoclassi, override dei metodi, super(), ereditarietà multipla, MRO e controlli isinstance — con esempi eseguibili.
Comprendere l'ereditarietà in Python
L'ereditarietà ti permette di costruire una nuova classe a partire da una esistente. La nuova classe (la sottoclasse o classe figlia) acquisisce automaticamente tutti gli attributi e i metodi della classe esistente (la superclasse, classe base o classe genitore). Puoi quindi aggiungere comportamenti extra o sovrascrivere selettivamente ciò che hai ereditato.
Questo capitolo tratta:
- La sintassi per creare sottoclassi
- Come funziona
super()e perché dovresti usarlo - L'override dei metodi e come richiamare la versione del genitore
- Ereditarietà singola, multilivello e multipla
- Il Method Resolution Order (MRO) di Python
isinstance()eissubclass()per i controlli di tipo a runtime- Errori comuni
Prima di leggere questo capitolo, assicurati di conoscere bene le classi e gli oggetti Python. Per gli altri pilastri della OOP correlati, consulta Incapsulamento Python e Classi Astratte Python.
La Sintassi dell'Ereditarietà
Per creare una sottoclasse, scrivi il nome della classe genitore tra parentesi dopo il nome della nuova classe:
class ParentClass:
pass
class ChildClass(ParentClass):
passUn esempio concreto ma minimale con una classe base Vehicle e una sottoclasse Car:
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def start(self):
print(f"{self.make} {self.model} started.")
def stop(self):
print(f"{self.make} {self.model} stopped.")
class Car(Vehicle):
def __init__(self, make, model, year, num_doors=4):
super().__init__(make, model, year) # delegate to Vehicle.__init__
self.num_doors = num_doors
camry = Car("Toyota", "Camry", 2023)
camry.start() # Toyota Camry started.
camry.stop() # Toyota Camry stopped.
print(camry.num_doors) # 4Car eredita start() e stop() da Vehicle senza duplicarli. Aggiungere num_doors è l'unica nuova responsabilità che Car deve gestire.
Usare super()
super() restituisce un oggetto proxy che delega le chiamate ai metodi alla classe genitore. L'uso più comune è all'interno di __init__ per inizializzare gli attributi del genitore prima di aggiungere quelli propri della classe figlia:
class Car(Vehicle):
def __init__(self, make, model, year, num_doors=4):
super().__init__(make, model, year) # runs Vehicle.__init__
self.num_doors = num_doorsPerché preferire super() rispetto a scrivere direttamente Vehicle.__init__(self, ...)?
- Manutenibilità: se rinomini o sostituisci la classe genitore, devi modificare solo una riga.
- Ereditarietà multipla:
super()segue il Method Resolution Order (MRO) di Python, quindi ogni classe nella catena viene chiamata correttamente. Specificare direttamente il nome del genitore salta questa logica.
Puoi chiamare super() per qualsiasi metodo, non solo __init__:
class Car(Vehicle):
def start(self):
super().start() # call Vehicle.start first
print(f"({self.num_doors}-door model ready)")Override dei Metodi
Una sottoclasse sovrascrive un metodo del genitore definendo un metodo con lo stesso nome. Python chiama sempre prima la versione più derivata:
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def start(self):
print(f"{self.make} {self.model} started.")
def stop(self):
print(f"{self.make} {self.model} stopped.")
class Car(Vehicle):
def start(self):
print(f"{self.make} {self.model} revved the engine and started.")
camry = Car("Toyota", "Camry", 2023)
camry.start() # Toyota Camry revved the engine and started.
camry.stop() # Toyota Camry stopped. (inherited, not overridden)Quando vuoi estendere piuttosto che sostituire il comportamento del genitore, chiama super() all'interno dell'override:
class Car(Vehicle):
def start(self):
super().start() # Vehicle.start runs first
print("Seatbelt reminder: buckle up!") # then add the extra stepEreditare Attributi di Classe
L'ereditarietà si applica anche agli attributi di classe (condivisi tra le istanze). Una sottoclasse può sovrascriverli nello stesso modo in cui sovrascrive i metodi:
class Vehicle:
wheels = 4
class Motorcycle(Vehicle):
wheels = 2 # override the class attribute
class Car(Vehicle):
pass # inherits wheels = 4
print(Motorcycle.wheels) # 2
print(Car.wheels) # 4
print(Vehicle.wheels) # 4Tipi di Ereditarietà
Ereditarietà Singola
Un figlio, un genitore — il pattern più comune.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
class Dog(Animal):
def speak(self):
return f"{self.name} says woof!"
dog = Dog("Rex")
print(dog.speak()) # Rex says woof!Ereditarietà Multilivello
Una sottoclasse può a sua volta essere sottoclassata, creando una catena:
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def stop(self):
print(f"{self.make} {self.model} stopped.")
class Car(Vehicle):
def __init__(self, make, model, year, num_doors=4):
super().__init__(make, model, year)
self.num_doors = num_doors
def start(self):
print(f"{self.make} {self.model} started.")
class ElectricCar(Car):
def __init__(self, make, model, year, range_km):
super().__init__(make, model, year) # calls Car.__init__
self.range_km = range_km
def start(self):
print(f"{self.make} {self.model} powered up silently.")
def charge(self):
print(f"Charging... range is {self.range_km} km.")
tesla = ElectricCar("Tesla", "Model 3", 2024, 580)
tesla.start() # Tesla Model 3 powered up silently.
tesla.charge() # Charging... range is 580 km.
tesla.stop() # Tesla Model 3 stopped. (inherited from Vehicle)ElectricCar si trova due livelli sotto Vehicle. Ogni chiamata a super().__init__ sale di un livello nella catena, quindi tutti e tre i metodi __init__ vengono eseguiti.
Ereditarietà Multipla
Python permette a una classe di ereditare da più di un genitore. Elencali tra parentesi, separati da virgole:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
class Swimmer:
def swim(self):
return f"{self.name} swims."
class Flyer:
def fly(self):
return f"{self.name} flies."
class Duck(Animal, Swimmer, Flyer):
def speak(self):
return f"{self.name} says quack!"
donald = Duck("Donald")
print(donald.speak()) # Donald says quack!
print(donald.swim()) # Donald swims.
print(donald.fly()) # Donald flies.L'ereditarietà multipla è potente ma introduce complessità. Usa i mixin — classi piccole e a scopo singolo che aggiungono una sola capacità — per mantenerla gestibile. Swimmer e Flyer qui sopra seguono esattamente questo pattern.
Il Method Resolution Order (MRO)
Quando Python cerca un metodo o un attributo, segue un ordine di ricerca deterministico chiamato Method Resolution Order (MRO). Per l'ereditarietà singola l'ordine è ovvio (figlio → genitore → nonno → object). Per l'ereditarietà multipla Python usa l'algoritmo di linearizzazione C3 per produrre un ordine non ambiguo.
Puoi ispezionare l'MRO di qualsiasi classe con .__mro__ o help():
print(Duck.__mro__)
# (<class 'Duck'>, <class 'Animal'>, <class 'Swimmer'>, <class 'Flyer'>, <class 'object'>)L'MRO è più importante quando più genitori definiscono lo stesso metodo. Python sceglie la prima classe nell'MRO che lo definisce. Ecco perché super() è importante nelle gerarchie con ereditarietà multipla: ogni classe nell'MRO chiama super(), così ogni classe nella catena ha la possibilità di essere eseguita.
class Base:
def greet(self):
print("Hello from Base")
class Left(Base):
def greet(self):
print("Hello from Left")
super().greet()
class Right(Base):
def greet(self):
print("Hello from Right")
super().greet()
class Child(Left, Right):
def greet(self):
print("Hello from Child")
super().greet()
Child().greet()
# Hello from Child
# Hello from Left
# Hello from Right
# Hello from BaseOgni chiamata a super() segue l'MRO (Child → Left → Right → Base), quindi ogni greet() viene eseguito esattamente una volta nonostante Base appaia come genitore sia di Left che di Right. Questo è il problema del diamante — l'MRO di Python lo risolve in modo pulito.
Verifica dell'Ereditarietà a Runtime
isinstance(obj, cls)
Restituisce True se obj è un'istanza di cls o di qualsiasi sua sottoclasse:
tesla = ElectricCar("Tesla", "Model 3", 2024, 580)
print(isinstance(tesla, ElectricCar)) # True
print(isinstance(tesla, Car)) # True — Car is a parent
print(isinstance(tesla, Vehicle)) # True — Vehicle is a grandparent
print(isinstance(tesla, str)) # FalseQuesto è più affidabile rispetto al confronto type(obj) == Car, che restituisce False per le sottoclassi.
issubclass(sub, cls)
Restituisce True se sub è una sottoclasse di cls (incluso se stesso):
print(issubclass(ElectricCar, Vehicle)) # True
print(issubclass(Car, ElectricCar)) # False
print(issubclass(Car, Car)) # True — a class is a subclass of itselfErrori Comuni
Dimenticare di chiamare super().__init__()
Se il metodo __init__ della classe figlia non chiama super().__init__(), gli attributi del genitore non vengono mai impostati. Qualsiasi metodo che li utilizza solleverà un AttributeError:
class Car(Vehicle):
def __init__(self, make, model, year, num_doors=4):
# forgot super().__init__(...)
self.num_doors = num_doors
c = Car("Toyota", "Camry", 2023)
c.start() # AttributeError: 'Car' object has no attribute 'make'Sovrascrivere senza chiamare super() quando si intendeva estendere
Se sovrascrivi un metodo e dimentichi super(), la versione del genitore non viene mai eseguita. Questo elimina silenziosamente un comportamento dal quale altro codice potrebbe dipendere.
Gerarchie di ereditarietà multipla profonde
Più di due livelli di ereditarietà multipla è molto difficile da ragionare. Se ti ritrovi a scrivere class Foo(A, B, C, D), considera di usare la composizione — memorizzando le istanze come attributi — al suo posto.
Vantaggi dell'Ereditarietà
- Riutilizzo del codice — la logica condivisa vive in un unico posto; le sottoclassi la ereditano gratuitamente.
- Estensibilità — puoi aggiungere o modificare il comportamento in una sottoclasse senza toccare il genitore, mantenendo invariati i chiamanti esistenti.
- Polimorfismo — le funzioni che accettano un
Vehiclefunzionano ugualmente bene con qualsiasiCar,MotorcycleoElectricCar. Consulta Polimorfismo Python per il quadro completo.