03: L'Ereditarietà - Programmare in Python

Video Corso Programmazione a Oggetti Python 3

03: L'Ereditarietà

Bentornati ragazzi e ragazze!

In questa lezione continuiamo a parlare di Classi introducendo il concetto di Ereditarietà.

Proprio come un bambino eredita dai gentitori 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 sempre un'applicazione per una scuola, ma che sia stavolta in grado di gestire sia studenti che insegnanti.

Si tratta di un esempio tipico su cui poter testare l'ereditarietà. Pensateci bene.

Tra studendi e insegnanti ci sono chiaramente delle differenze, ad esempio uno studente starà seguendo un indirizzo_di_studio, mentre un insegnante in quanto tale, avrà magari un elenco delle materie che insegna.

Sia studenti che insegnanti sono peró persone, giusto? entrambi hanno nomi, cognomi, età, un indirizzo di residenza, ed entrambi avranno anche una scheda personale, giusto?

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 --> ")

Quindi ora, per creare le nostre sottoclassi ci basta crearne due nuove come abbiamo fatto fin'ora, 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)

Bene, ora che stiamo iniziando ad avere familiarità col concetto direi di andare avanti.

Come abbiamo detto a inizio video, Studenti e Insegnanti, per quanto simili, hanno comunque delle caratteristiche diverse molto importanti. Direi anzitutto di iniziare aggiungendo una variabile profilo per ciascuna delle sottoclassi. Inoltre, al momento delle creazione di ciascun oggetto, vogliamo chiaramente aggiungere 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 le due sottoclassi.

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'indirizzo_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'indirizzo_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 overwriting, ovvero come abbiamo accennato in precedenza 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

Bene, ora stiamo iniziando ad avere del codice abbastanza soddisfacente.

Un'altra cosa che possiamo fare, è creare un metodo per la classe Studente che ci permetta di modificare il corso di studio, e per la classe insegnante, che ci consenta di aggiungere delle materie a quelle già 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")



Menu della Serie