W3docs

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() e issubclass() 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):
    pass

Un 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)  # 4

Car 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_doors

Perché 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 step

Ereditare 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)     # 4

Tipi 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 Base

Ogni 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))          # False

Questo è 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 itself

Errori 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à

  1. Riutilizzo del codice — la logica condivisa vive in un unico posto; le sottoclassi la ereditano gratuitamente.
  2. Estensibilità — puoi aggiungere o modificare il comportamento in una sottoclasse senza toccare il genitore, mantenendo invariati i chiamanti esistenti.
  3. Polimorfismo — le funzioni che accettano un Vehicle funzionano ugualmente bene con qualsiasi Car, Motorcycle o ElectricCar. Consulta Polimorfismo Python per il quadro completo.

Pratica

Pratica
Which of the following statements about Python inheritance are correct according to the information provided on the specified URL?
Which of the following statements about Python inheritance are correct according to the information provided on the specified URL?
Was this page helpful?