3. L'Ereditarietà

In questa lezione continuiamo a parlare di classi introducendo il concetto di ereditarietà.

Proprio come un bambino eredita dai genitori alcuni tratti distintivi come colore degli occhi e dei capelli, in informatica l'ereditarietà ci consente di creare classi figlie a partire da classi genitore, ereditandone in questa maniera gli attributi ed i metodi. Permettendo questo genere di riutilizzo del codice scritto, l'ereditarietà si dimostra estremamente utile nello snellire i nostri programmi. Possiamo inoltre aggiungere nuovi metodi e proprietà alle nostre classi figlie lasciando così invariata la classe genitore.

Nelle due lezioni precedenti a queste abbiamo lavorato su una classe Studente: immaginiamo ora di voler scrivere un'applicazione per una scuola, ma che sia anche in grado di gestire sia studenti che insegnanti: tra questi ci sono chiaramente delle differenze, ad esempio uno studente starà seguendo un indirizzo_di_studio, mentre un insegnante avrà un elenco delle materie che insegna.

Sia studenti che insegnanti sono però persone, entrambi hanno nomi, cognomi, età, un indirizzo di residenza, ed entrambi avranno anche una scheda personale, quindi ciò che ora faremo sarà creare una classe genitore chiamata Persona, in cui specificheremo le caratteristiche comuni ai due, che verranno poi ereditate dalle due sottoclassi figlie, la sottoclasse Studente e la sottoclasse Insegnante.

class Persona:

    def __init__(self, nome, cognome, età, residenza):
        self.nome = nome
        self.cognome = cognome
        self.età = età
        self.residenza = residenza

    def scheda_personale(self):
        scheda = f"""
        Nome: {self.nome} 
        Cognome: {self.cognome}
        Età: {self.età}
        Residenza: {self.residenza}\n"""
        return scheda

    def modifica_scheda(self):
        print("""Modifica Scheda:
        1 - Nome
        2 - Cognome
        3 - Età
        4 - Residenza""")
        scelta = input("Cosa Desideri modificare?")
        if scelta == "1":
            self.nome = input("Nuovo Nome--> ")
        elif scelta == "2":
            self. cognome = input("Nuovo Cognome --> ")
        elif scelta == "3":
            self.età = int(input("Nuova età --> "))
        elif scelta == "4":
            self.residenza = input("Nuova Residenza --> ")

Per creare le nostre sottoclassi ci basta crearne due nuove come abbiamo fatto finora, ma con una piccola modifica:

class Studente(Persona):
    pass

class Insegnante(Persona):
    pass

Come vedete ho aggiunto delle parentesi, e tra queste parentesi ho passato il nome della classe genitore da cui voglio ereditare.

In questo caso specifico ho anche aggiunto pass, la cui unica funzione è agire da sostituto per il resto del codice che ancora non abbiamo scritto, e ho fatto questo per mostrarvi la prima magia dell'ereditarietà.

Siamo infatti già in grado di istanziare sia Studente che Insegnante senza aver ancora scritto nulla al loro interno, e questo proprio perché stanno ereditando attributi e metodi da Persona:

studente_uno = Studente("Py", "Mike", 24, "Casa Dello Studente")
insegnante_uno = Insegnante("Mario", "Rossi", 33, "Viale Roma 32")

print(studente_uno.scheda_personale())
print(insegnante_uno.scheda_personale())

  Nome: Py 
  Cognome: Mike
  Età: 24
  Residenza: Casa Dello Studente

  Nome: Mario 
  Cognome: Rossi
  Età: 33
  Residenza: Viale Roma 32

Quindi ciò che è successo è che Python è andato a cercare il metodo costruttore __init__ all'interno delle due classi che abbiamo istanziato, e non avendolo trovato è andato a prenderselo da Persona.

Per darvi una visuale più concreta del concetto possiamo richiamare la funzione help() passandole come parametro una delle sottoclassi:

Help on class Insegnante in module __main__:

class Insegnante(Persona)
 |  Method resolution order:
 |      Insegnante
 |      Persona
 |      builtins.object
 |  
 |  Methods inherited from Persona:
 |  
 |  __init__(self, nome, cognome, età, residenza)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  modifica_scheda(self)
 |  
 |  scheda_personale(self)
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from Persona:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)


La funzione super()

Studenti e insegnanti, per quanto simili, hanno comunque delle caratteristiche diverse: aggiungiamo una variabile profilo per ciascuna delle sottoclassi, il corso_di_studio seguito per lo Studente e un elenco delle materie insegnate per l'Insegnante: per fare questo dobbiamo creare una versione personalizzata del metodo __init__ per entrambe.

Per mantenere la semplicità nel nostro codice utilizziamo la funzione super() per far in modo che nome, cognome, ed età vengano gestiti dal metodo __init__ della classe genitore, ovvero la classe Persona. Le sottoclassi Studente e Insegnante gestiranno rispettivamente il corso_di_studio e l'elenco delle materie.

class Studente(Persona):
    profilo = "Studente"

    def __init__(self, nome, cognome, età, residenza, corso_di_studio):
        super().__init__(nome, cognome, età, residenza)
        self.corso_di_studio = corso_di_studio


class Insegnante(Persona):
    profilo = "Insegnante"

    def __init__(self, nome, cognome, età, residenza, materie=None):
        super().__init__(nome, cognome, età, residenza)
        if materie is None:
            self.materie = []
        else:
            self.materie = materie

Per mantenere la semplicità nel nostro codice utilizziamo la funzione super() per far in modo che nome, cognome, ed età vengano gestiti dal metodo __init__ della classe genitore, ovvero la classe Persona. Le sottoclassi Studente e Insegnante gestiranno rispettivamente l'corso_di_studio e l'elenco delle materie.

Ora che abbiamo aggiunto delle nuove caratteristiche, ovvero profilo, corso_di_studio e materie, sarà anche il caso di visualizzarle nella scheda personale. In questo caso una delle scelte più convenienti è quella di creare una versione personalizzata del metodo scheda_personale() per ciascuna delle sottoclassi. Nel mondo della programmazione a oggetti chiamiamo questo overriding, ovvero la possibilità per una sottoclasse di avere un'implementazione differente di un metodo della classe genitore.

class Studente(Persona):
    profilo = "Studente"

    def __init__(self,nome, cognome, età, residenza, corso_di_studio):
        super().__init__(nome, cognome, età, residenza)
        self.corso_di_studio = corso_di_studio

    def scheda_personale(self):
        scheda = f"""
        Profilo:{Studente.profilo}
        Corso di Studi:{self.corso_di_studio}
        ***"""
        return super().scheda_personale() + scheda


class Insegnante(Persona):
    profilo = "Insegnante"

    def __init__(self,nome, cognome, età, residenza, materie=None):
        super().__init__(nome, cognome, età, residenza)
        if materie is None:
            self.materie = []
        else:
            self.materie = materie

    def scheda_personale(self):
        scheda = f"""
        Profilo:{Insegnante.profilo}
        Materie Insegnate:{self.materie}
        ***"""
        return super().scheda_personale() + scheda

Un'altra cosa che possiamo fare è creare un metodo per la classe Studente che ci permetta di modificare il corso_di_studio, e uno per la classe Insegnante che ci consenta di aggiungere delle materie a quelle insegnate.

class Studente(Persona):
    profilo = "Studente"

     def __init__(self,nome, cognome, età, residenza, corso_di_studio):
        super().__init__(nome, cognome, età, residenza)
        self.corso_di_studio = corso_di_studio

    def scheda_personale(self):
        scheda = f"""
        Profilo:{Studente.profilo}
        Corso di Studi:{self.corso_di_studio}
        ***"""
        return super().scheda_personale() + scheda

    def cambio_corso(self,corso):
        self.corso_di_studio = corso
        print(f"Corso Aggiornato")


class Insegnante(Persona):
    profilo = "Insegnante"

    def __init__(self,nome, cognome, età, residenza, materie=None):
        super().__init__(nome, cognome, età, residenza)
        if materie is None:
            self.materie = []
        else:
            self.materie = materie

    def scheda_personale(self):
        scheda = f"""
        Profilo:{Insegnante.profilo}
        Materie Insegnate:{self.materie}
        ***"""
        return super().scheda_personale() + scheda

    def aggiungi_materia(self,nuova):
        if nuova not in self.materie:
            self.materie.append(nuova)
        print("Elenco Aggiornato")