Immagina di costruire un sistema software con più ruoli utente come studenti, insegnanti e amministratori. Questi ruoli condividono attributi comuni come nome e ID, ma richiedono anche funzionalità specifiche. Invece di duplicare il codice, l’ereditarietà ti permette di definire il comportamento condiviso in una classe padre ed estenderlo in classi figlie specializzate.
In questo articolo esploreremo l’ereditarietà in Python, coprendo concetti di base e avanzati come il method overriding e la funzione super(), una funzione built-in che restituisce un oggetto temporaneo della superclasse, così puoi accedere ai suoi metodi senza nominare esplicitamente la classe padre. Non preoccuparti se non è ancora chiaro: tra poco vedremo tutto nel dettaglio.
Basi dell’ereditarietà in Python
L’ereditarietà è uno dei pilastri della programmazione orientata agli oggetti (OOP) che permette a una classe (chiamata classe figlia) di derivare attributi e metodi da un’altra classe (chiamata classe padre). Questa caratteristica è centrale per il riuso del codice e semplifica la manutenzione, rendendo più semplice creare programmi scalabili ed efficienti.
Definire classi padre e figlie
Prima di procedere, esploriamo la relazione tra classi padre e figlie.
Classe padre
Iniziamo dalla classe padre. Una classe padre è la classe base da cui derivano le classi figlie. Incapsula attributi e metodi condivisi.
In Python, ecco come definiamo una classe padre:
class ParentClass:
def __init__(self, attributes):
# Initialize attributes
pass
def method(self):
# Define behavior
pass
Classe figlia
Una classe figlia eredita attributi e metodi dalla classe padre. Questo le consente di usare la funzionalità definita nella classe padre. Il codice seguente mostra come una classe figlia eredita attributi e metodi da una classe padre:
class ChildClass(ParentClass):
def additional_method(self):
# Define new behavior
pass
Questa semplice sintassi permette alla classe figlia di utilizzare ed estendere la funzionalità definita nella classe padre.
Creare una classe padre e una classe figlia
Creiamo un esempio pratico con una classe Person come padre e una classe Student come figlia.
Creare la classe padre
La classe Person contiene attributi condivisi e un metodo per mostrare le informazioni:
# Defining the Parent Class
class Person:
def __init__(self, name, id):
self.name = name
self.id = id
def display_info(self):
return f"Name: {self.name}, ID: {self.id}"
Creare la classe figlia
La classe Student eredita da Person e aggiunge un nuovo metodo study.
# Defining the Child Class
class Student(Person):
def study(self):
return f"{self.name} is studying."
Mettiamo alla prova le classi padre e figlia.
# Creating and Testing Instances
student = Student("Samuel", 102)
print(student.display_info())
print(student.study())
Name: Samuel, ID: 102
Samuel is studying.
Ecco cosa succede:
-
La classe
Studentusa il metodo__init__diPersonper inizializzarenameeid. -
Il metodo
studyè esclusivo della classeStudente ne estende la funzionalità. -
Il metodo
display_infoè ereditato direttamente daPerson.
Tipi di ereditarietà in Python
L’ereditarietà in Python permette alle classi di ereditare attributi e comportamenti da altre classi, favorendo il riuso del codice e un design pulito, come visto prima. In questa sezione parleremo dei diversi tipi di ereditarietà in Python: singola, multipla, gerarchica e ibrida come categorie distinte.
Ereditarietà singola
L’ereditarietà singola si verifica quando una classe figlia eredita da una sola classe padre, consentendole di estenderne la funzionalità. È utile quando un tipo di oggetto condivide proprietà comuni con una categoria più ampia ma richiede anche attributi o comportamenti aggiuntivi.
L’esempio visto in precedenza era un’ereditarietà singola, ma ora guardiamolo più da vicino: in un sistema di gestione scolastica, tutte le persone, inclusi studenti, insegnanti e personale, condividono dettagli comuni come name e ID. Tuttavia, gli studenti hanno anche dati accademici come voti e corsi frequentati. Con l’ereditarietà singola, possiamo creare una classe Person per gli attributi condivisi ed estenderla con una classe Student per i dettagli accademici.

Ereditarietà singola. Immagine dell’autore
Ecco un buon esempio dello scenario descritto:
class Person:
"""Represents a general person with basic details."""
def __init__(self, name, id):
self.name = name
self.id = id
def get_details(self):
return f"Name: {self.name}, ID: {self.id}"
class Student(Person):
"""Represents a student, extending the Person class to include academic details."""
def __init__(self, name, id, grade, courses):
super().__init__(name, id)
self.grade = grade
self.courses = courses
def get_details(self):
return f"Name: {self.name}, ID: {self.id}, Grade: {self.grade}, Courses: {', '.join(self.courses)}"
# Example usage in a school system
student = Student("Samuel", 5678, "B+", ["Math", "Physics", "Computer Science"])
print(student.get_details())
Name: Samuel, ID: 5678, Grade: B+, Courses: Math, Physics, Computer Science
La classe Student eredita il metodo get_details() da Person ma lo estende includendo grade e courses. È un buon esempio di come l’ereditarietà singola promuove quello che chiamiamo codice modulare.
Ereditarietà multipla
L’ereditarietà multipla, in un certo senso come un albero genealogico, permette a una classe figlia di ereditare da più classi padre, combinandone attributi e comportamenti. Questo può generare conflitti potenziali, che Python risolve usando l’ordine di risoluzione dei metodi (MRO).

Ereditarietà multipla. Immagine dell’autore
Guarda qui:
class Person:
def get_details(self):
return "Details of a person."
class Athlete:
def get_skill(self):
return "Athletic skills."
class Student(Person, Athlete):
pass
# Example usage
student = Student()
print(student.get_details())
print(student.get_skill())
Details of a person.
Athletic skills.
Vediamo che la classe Student ha ereditato attributi e metodi sia da Person sia da Athlete. Senza ulteriore sforzo, la classe Student ha accesso al metodo get_details() della classe padre Person e al metodo get_skill() della classe padre Athlete. Stiamo di fatto combinando funzionalità da più fonti.
Tuttavia, ereditare da più classi può causare conflitti. E se entrambe le classi padre definiscono un metodo o un attributo con lo stesso nome? Ho accennato prima all’ordine di risoluzione dei metodi: aggiungo qualche dettaglio. L’ordine di risoluzione dei metodi determina l’ordine in cui le classi vengono cercate per metodi e attributi. L’MRO segue un approccio depth-first, da sinistra a destra.
Puoi visualizzare l’MRO di una classe usando l’attributo __mro__ o il metodo mro():
print(Student.__mro__)
(<class '__main__.Student'>, <class '__main__.Person'>, <class '__main__.Athlete'>, <class 'object'>)
Ereditarietà multilivello, gerarchica e ibrida
Python supporta anche strutture di ereditarietà più complesse. Userò lo stesso esempio per mostrarle.
Ereditarietà multilivello
L’ereditarietà multilivello si verifica quando una classe figlia eredita da un’altra classe figlia, e quella classe figlia eredita da una classe padre. Si crea così una catena di ereditarietà.

Ereditarietà multilivello. Immagine dell’autore
Ecco un buon esempio:
class Person:
def __init__(self, name, id):
self.name = name
self.id = id
def get_details(self):
return f"Name: {self.name}, ID: {self.id}"
class Student(Person):
def __init__(self, name, id, grade):
super().__init__(name, id)
self.grade = grade
def get_details(self):
return f"Name: {self.name}, ID: {self.id}, Grade: {self.grade}"
class GraduateStudent(Student):
def __init__(self, name, id, grade, thesis_title):
super().__init__(name, id, grade)
self.thesis_title = thesis_title
def get_details(self):
return f"Name: {self.name}, ID: {self.id}, Grade: {self.grade}, Thesis: {self.thesis_title}"
# Example usage
grad_student = GraduateStudent("Charlie", 91011, "A", "AI in Healthcare")
print(grad_student.get_details())
Name: Charlie, ID: 91011, Grade: A, Thesis: AI in Healthcare
Qui ogni classe nella catena aggiunge qualcosa di nuovo: Person gestisce nomi e ID, Student include i voti e GraduateStudent introduce una tesi. Grazie a super().__init__(), riutilizziamo la logica di inizializzazione senza duplicare il codice. È efficiente, pulito e fa sì che ogni livello della “scala dell’ereditarietà”, come mi piace pensarla, funzioni.
Ereditarietà gerarchica
Nell’ereditarietà gerarchica, più classi figlie ereditano da un’unica classe padre, consentendo comportamenti condivisi tra sottoclassi con attributi unici.

Ereditarietà gerarchica. Immagine dell’autore
Vediamo insieme un buon esempio:
class Person:
def __init__(self, name, id):
self.name = name
self.id = id
def get_details(self):
return f"Name: {self.name}, ID: {self.id}"
class Student(Person):
def __init__(self, name, id, grade):
super().__init__(name, id)
self.grade = grade
def get_details(self):
return f"Name: {self.name}, ID: {self.id}, Grade: {self.grade}"
class Teacher(Person):
def __init__(self, name, id, subject):
super().__init__(name, id)
self.subject = subject
def get_details(self):
return f"Name: {self.name}, ID: {self.id}, Subject: {self.subject}"
# Example usage
student = Student("Samuel", 5678, "B+")
teacher = Teacher("Dr. Smith", 1234, "Math")
print(student.get_details())
print(teacher.get_details())
Name: Samuel, ID: 5678, Grade: B+
Dr. Smith, ID: 1234, Subject: Math
Qui la classe Person funge da base, offrendo attributi e metodi comuni (name, id e get_details). Le classi Student e Teacher estendono poi questa funzionalità aggiungendo proprietà uniche (grade e subject) e personalizzando il metodo get_details in base al proprio contesto.
Con questo approccio, la funzionalità condivisa rimane in un unico punto (la classe Person), mentre il comportamento specializzato è ben incapsulato nelle sottoclassi.
Ereditarietà ibrida
L’ereditarietà ibrida combina più tipi di ereditarietà, come multilivello o multipla, per modellare relazioni più complesse.

Ereditarietà ibrida. Immagine dell’autore
Vediamo un esempio che mostra la complessità dell’ereditarietà ibrida.
# Base class
class Person:
def __init__(self, name, id):
self.name = name
self.id = id
def get_details(self):
return f"Name: {self.name}, ID: {self.id}"
# Intermediate class inheriting from the base class
class Employee(Person):
def __init__(self, name, id, position):
super().__init__(name, id)
self.position = position
def get_position(self):
return f"Position: {self.position}"
# Another independent base class
class Athlete:
def __init__(self, sport):
self.sport = sport
def get_sport(self):
return f"Sport: {self.sport}"
# Derived class combining Employee and Athlete
class Student(Employee, Athlete):
def __init__(self, name, id, position, grade, sport):
Employee.__init__(self, name, id, position)
Athlete.__init__(self, sport)
self.grade = grade
def get_grade(self):
return f"Grade: {self.grade}"
# Example usage
student = Student("Samuel", 1234, "Intern", "A", "Soccer")
print(student.get_details()) # From Person
print(student.get_position()) # From Employee
print(student.get_grade()) # From Student
print(student.get_sport()) # From Athlete
Name: Samuel, ID: 1234
Position: Intern
Grade: A
Sport: Soccer
In questo esempio, la classe Student dimostra ereditarietà ibrida ereditando attributi e metodi da Employee (che a sua volta eredita da Person) e da Athlete. Questo combina ereditarietà gerarchica (dove Employee eredita da Person) ed ereditarietà multipla (dove Student eredita sia da Employee sia da Athlete).
Vantaggi dell’ereditarietà in Python
Ora è il momento di vedere punti di forza e debolezze:
Vantaggi dell’ereditarietà
-
Riutilizzabilità: con l’ereditarietà puoi scrivere il codice una volta nella classe padre e riusarlo nelle classi figlie. Nell’esempio, sia
FullTimeEmployeesiaContractorpossono ereditare un metodoget_details()dalla classe padreEmployee. -
Semplicità: l’ereditarietà modella chiaramente le relazioni. Un buon esempio è la classe
FullTimeEmployeeche “è un” tipo della classe padreEmployee. -
Scalabilità: permette anche di aggiungere nuove funzionalità o classi figlie senza influire sul codice esistente. Ad esempio, possiamo aggiungere facilmente una nuova classe
Interncome classe figlia.
Possibili limitazioni dell’ereditarietà
-
Complessità: non sorprende, ma troppi livelli di ereditarietà possono rendere il codice difficile da seguire. Per esempio, se un
Employeeha troppe classi figlie comeManager,Engineer,Intern, ecc., può diventare confuso. -
Dipendenza: le modifiche a una classe padre possono influenzare involontariamente tutte le sottoclassi. Se modifichi, ad esempio,
Employee, potresti rompereFullTimeEmployeeoContractor. -
Uso improprio: usare l’ereditarietà quando non è la scelta migliore può complicare i design. Non vorresti creare una soluzione in cui
Careredita daBoatsolo per riusaremove(). La relazione non ha senso.
Tecniche avanzate di ereditarietà in Python
Ora che abbiamo visto le basi dell’ereditarietà, analizziamo alcune tecniche avanzate. Queste tecniche, come il method overriding, super(), le classi astratte di base e il polimorfismo, aumentano la flessibilità del codice e permettono pattern di progettazione più sofisticati.
Override dei metodi in Python
L’override dei metodi permette a una classe figlia di fornire una specifica implementazione per un metodo già definito nella classe padre. È utile quando il comportamento ereditato non soddisfa pienamente i requisiti della classe figlia.
class Person:
def __init__(self, name, id):
self.name = name
self.id = id
def get_details(self):
return f"Name: {self.name}, ID: {self.id}"
class Student(Person):
def __init__(self, name, id, grade):
super().__init__(name, id)
self.grade = grade
# Overriding the get_details method
def get_details(self):
return f"Name: {self.name}, ID: {self.id}, Grade: {self.grade}"
# Example usage
student = Student("Samuel", 1234, "A")
print(student.get_details())
Name: Samuel, ID: 1234, Grade: A
Qui la classe Student esegue l’override del metodo get_details() della classe Person con una propria implementazione specifica. Questo permette alla classe figlia di avere un comportamento proprio pur mantenendo lo stesso nome del metodo.
Perché facciamo override? Per personalizzare il comportamento ereditato e per adattare la funzionalità di un metodo della classe padre alle esigenze specifiche della classe figlia.
Usare super() per l’inizializzazione del padre
La funzione super() viene usata per chiamare metodi della classe padre dalla classe figlia. È particolarmente utile quando vuoi estendere o modificare la funzionalità di un metodo della classe padre, come il costruttore __Init__().
Perché usiamo la funzione super()? La usiamo per chiamare e inizializzare il costruttore della classe padre e anche per evitare di nominare esplicitamente la classe padre. Questo è utile soprattutto nei casi di ereditarietà multipla.
class Person:
def __init__(self, name, id):
self.name = name
self.id = id
class Student(Person):
def __init__(self, name, id, grade):
# Using super() to initialize the parent class
super().__init__(name, id)
self.grade = grade
# Example usage
student = Student("Samuel", 5678, "B+")
print(student.name)
print(student.id)
print(student.grade)
Samuel
5678
B+
Qui la classe Student usa super().__init__(name, id) per chiamare il metodo __init__ della classe padre Person, così non deve ripetere il codice per inizializzare gli attributi name e id. La classe figlia introduce poi un attributo grade, specifico della classe Student.
Classi astratte di base (ABC)
Una abstract base class (ABC) è una classe che non può essere usata direttamente per creare oggetti. Serve a definire un insieme comune di metodi che altre classi dovrebbero implementare. Le ABC sono utili quando vuoi assicurarti che certi metodi siano sempre presenti nelle classi figlie.
from abc import ABC, abstractmethod
class Person(ABC):
@abstractmethod
def get_details(self):
pass
class Student(Person):
def __init__(self, name, id, grade):
self.name = name
self.id = id
self.grade = grade
def get_details(self):
return f"Name: {self.name}, ID: {self.id}, Grade: {self.grade}"
# Example usage
student = Student("Hamilton", 7890, "A-")
print(student.get_details())
Name: Hamilton, ID: 7890, Grade: A-
La classe Person qui è una classe astratta che richiede a qualsiasi classe figlia di implementare il metodo get_details(). Questo metodo viene poi implementato dalla classe figlia Student.
Polimorfismo
Polimorfismo significa molte forme. In Python permette a classi diverse di usare lo stesso nome di metodo, ma ognuna può implementarlo in modo diverso.
Il polimorfismo ci aiuta a scrivere codice che può funzionare con oggetti di classi differenti, anche se queste classi hanno comportamenti diversi:
class Person:
def get_details(self):
return "Details of a person."
class Student(Person):
def get_details(self):
return "Details of a student."
class Teacher(Person):
def get_details(self):
return "Details of a teacher."
# Example usage
def print_details(person):
print(person.get_details())
student = Student()
teacher = Teacher()
print_details(student)
print_details(teacher)
Details of a student.
Details of a teacher.
In questo esempio, la funzione print_details() può accettare qualsiasi oggetto di tipo Person, ma chiamerà il metodo get_details() appropriato a seconda che l’oggetto sia Student o Teacher.
Errori comuni e best practice
Sebbene l’ereditarietà sia potente, è facile usarla male. Condivido alcune idee per aiutarti a trarne il massimo.
Evitare sorprese con i metodi sovrascritti
Quando una classe figlia esegue l’override di un metodo del padre, il comportamento può cambiare.
Per esempio, se la classe padre Employee ha un metodo calculate_pay() e la classe figlia Manager lo sovrascrive senza considerare tutti gli scenari, potrebbe produrre calcoli errati della retribuzione.
La best practice, in questo caso, è testare sempre a fondo i metodi sovrascritti e documentarne il comportamento.
Scegliere tra ereditarietà e composizione
So che questo articolo parla di ereditarietà, ma non è sempre l’approccio giusto. A volte la composizione, cioè costruire classi combinando oggetti invece di estenderli, può essere più adatta a ciò che stai facendo.
Per distillare le differenze nel modo più basilare, pensa che:
-
Ereditarietà si riferisce a relazioni “è-un”. Per esempio, un
Managerè unEmployee. -
Composizione si riferisce a relazioni “ha-un”. Per esempio, un
Carha unEngine.
Quindi, come capire quando la composizione è l’approccio migliore? Usala quando la relazione non è strettamente gerarchica e/o quando vuoi ridurre l’accoppiamento stretto tra classi.
Oppure, si può anche dire che, mentre l’ereditarietà modella le relazioni, la composizione si concentra sulla funzionalità. Per aiutarti, considera questo:
-
Usa l’ereditarietà quando gli oggetti sono naturalmente gerarchici. Per esempio,
Animal > Bird > Parrot. -
Usa la composizione quando gli oggetti condividono funzionalità ma non sono correlati. Per esempio, una
Printere unoScannerusano entrambi unDeviceManager.
Evita catene di ereditarietà profonde
Catene di ereditarietà profonde (molti livelli di relazioni padre–figlia) possono rendere il codice difficile da leggere e manutenere. Questo è un problema perché le modifiche a una classe padre possono influenzare involontariamente molte classi figlie. Inoltre, il debug diventa complesso poiché il comportamento è distribuito su più livelli.
La best practice in questo caso è mantenere le gerarchie poco profonde. Inoltre, considera l’uso della composizione (come detto prima) o la suddivisione di una catena in gerarchie separate se sta diventando troppo profonda.
Conclusione
L’ereditarietà è un pilastro fondamentale della programmazione orientata agli oggetti che ti permette di creare codice riutilizzabile, modulare e scalabile. Se padroneggi l’ereditarietà, ti sarà più facile semplificare sistemi complessi.
Un buon modo per approfondire è provare a costruire strutture di ereditarietà nei tuoi progetti. Parti in modo semplice, poi sperimenta con gerarchie più complesse per vedere come funzionano nella pratica.
Se vuoi approfondire ancora, dai un’occhiata al nostro corso Programming Paradigm Concepts per comprendere meglio l’ereditarietà e altri concetti. Il nostro percorso di carriera Python Developer è anche una buona risorsa che offre un cammino completo per sviluppare competenze di programmazione avanzate utili per lo sviluppo software.
Professionista dei dati e scrittore con esperienza, appassionato nell’aiutare aspiranti esperti nel mondo dei dati.
Domande frequenti su Python Inheritance
Che cos’è l’ereditarietà in Python?
L’ereditarietà in Python è un meccanismo che consente a una classe di ereditare attributi e metodi da un’altra classe, favorendo il riuso del codice e strutture di classi gerarchiche.
Come funziona l’ereditarietà in Python?
L’ereditarietà in Python funziona definendo una nuova classe che eredita attributi e metodi da una classe esistente, consentendo strutture di classi gerarchiche.
Quali sono i tipi di ereditarietà in Python?
Python supporta diversi tipi di ereditarietà, tra cui ereditarietà singola, multipla, multilivello e gerarchica.
Qual è la differenza tra ereditarietà singola e multipla in Python?
L’ereditarietà singola prevede una sola classe padre, mentre l’ereditarietà multipla consente a una classe di ereditare da più classi padre.
Come si implementa l’ereditarietà in Python?
Implementa l’ereditarietà definendo una nuova classe che specifica una classe padre nella sua definizione, usando la sintassi class ChildClass(ParentClass):.
Quali sono i vantaggi dell’uso dell’ereditarietà in Python?
L’ereditarietà favorisce il riuso del codice, permette di creare strutture di classi gerarchiche e supporta il polimorfismo, rendendo il codice più flessibile e manutenibile.
Come si implementa l’ereditarietà multipla in Python?
L’ereditarietà multipla si implementa definendo una classe che eredita da più classi base, con Python che gestisce la risoluzione dei metodi usando l’ordine di risoluzione dei metodi (MRO).


