4. I Metodi di Classe

In questa lezione parleremo del concetto di metodo di classe nella programmazione a oggetti in Python.

Questa nuova tipologia di metodo va ad affiancarsi ai metodi "classici" che abbiamo visto finora. Come ricorderete, questi ultimi sono delle funzioni interne alle nostre classi, a cui viene sempre passato come primo parametro self, che rappresenta una referenza a ciascuna istanza della classe e che ci permette quindi di utilizzare il metodo su ciascun oggetto creato.

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à = input("Nuovo età --> ")
        elif scelta == "4":
            self.residenza = input("Nuovo residenza --> ")
            
            
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("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 Materie Aggiornato")

Tra gli usi dei metodi di classe, quello più comune è di costruttore alternativo, ovvero un metodo parallelo ad __init__ per istanziare oggetti e che possiamo utilizzare in alcuni contesti particolari.

Supponiamo che all'operatore incaricato di creare le varie istanze di Studente e Insegnante nel nostro programma venga passata una lista di persone su file testuale in cui nome, cognome, età e residenza siano separate da un punto e virgola, o da un trattino, in questo modo:

iron_man = "Tony-Stark-40-Torre Stark"


Il parametro cls e il decoratore @classmethod

In questo caso l'operatore dovrebbe quindi andare a copiarsi dal file testuale i vari attributi uno ad uno, stando attento a non copiare per sbaglio anche il trattino, e quindi passarli ad __init__. Per facilitare questo lavoro, il nostro costruttore alternativo sarà in grado di gestire questa casistica, prelevando i dati automaticamente da ciascuna stringa passata.

Il primo parametro ad essergli passato tra parentesi non è più l'Istanza, ma la classe stessa, quindi i metodi di classe sono legati alla classe e non alle Istanze.

Convenzionalmente parlando, come parametro non useremo quindi più self ma cls. I più attenti tra voi staranno ora pensando: Ma Mike, se si tratta solo di convenzioni, anche se cambi il nome al parametro, quella che viene passata resta sempre l'istanza, no? Be avete ragione.

Infatti, oltre a cambiare il nome del parametro, per fare in modo che Python passi effettivamente la classe invece che l'istanza, utilizziamo un decoratore, @classmethod cioè un comando specifico per lo scopo, in questo modo:

class Persona:

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

    @classmethod
    def costruttore_alternativo(cls):
        pass

    # ...

Parleremo di decoratori nel dettaglio in un'altra lezione, per ora basta sapere che questo fantastico strumento ci permette di alterare il comportamento dei metodi a nostro piacimento, e che @classmethod è il decoratore che si utilizza per creare i metodi di classe, e che permette quindi di passare come parametro la classe invece dell'istanza.

La convenzione in questi casi vuole inoltre che il nome sia abbastanza specifico sullo scopo del costruttore alternativo, lo chiameremo quindi come da prassi in questi casi, from_string(). Proseguiamo quindi con la scrittura del codice.

class Persona:

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

    @classmethod
    def from_string(cls, stringa_persona):
        nome, cognome, età, residenza = stringa_persona.split("-")
        return cls(nome, cognome, età, residenza)

Possiamo ora passare la stringa iron_man al nostro costruttore alternativo from_string, e creare un'istanza di Persona.

persona_uno = Persona.from_string(iron_man)
print(persona_uno.scheda_personale())

# output
  Nome: Tony
  Cognome: Stark
  Età: 40
  Residenza: Torre Stark

Abbiamo semplificato notevolmente il lavoro del nostro caro operatore o operatrice, riuscendo ad istanziare la persona di Tony Stark. Visto che anche questo metodo viene chiaramente ereditato automaticamente dalle sottoclassi di Persona, possiamo istanziare Iron Man come insegnante di ingegneria.

insg_uno = Insegnante.from_string(iron_man, "Ingegneria")

# output
TypeError: from_string() takes 2 positional arguments but 3 were given

Otteniamo un errore! Ma niente panico, analizziamolo: ci viene detto che abbiamo passato un parametro di troppo! L'insegnante ha anche le materie, ma queste sono definite nel SUO metodo costruttore __init__, e super() gestisce tutto il resto degli attributi, ricordate?

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


Il parametro *args

Visto che a seconda dei casi c'è la possibilità di avere dei parametri in più, talvolta in meno, utilizzeremo ora una piccola magia di Python:

@classmethod
def from_string(cls, stringa_ persona, *args):
    nome, cognome, età, residenza = stringa_persona.split("-")
    return cls(nome, cognome, età, residenza, *args)

*args è la notazione specifica in Python per casi come questi, in cui possono esserci zero oppure vari parametri aggiuntivi da passare alle nostre funzioni, ed è utilissima in contesti in cui si usa l'ereditarietà, permettendo ai nostri metodi di restare quanto più generici possibile.

iron_man = "Tony-Stark-40-Torre Stark"
zuck = "Mark-Zuckerberg-33-California"

insg_uno = Insegnante.from_string(iron_man,"Ingegneria")
stud_uno = Studente.from_string(zuck,"SEO")

print(insg_uno.scheda_personale())
print(stud_uno.scheda_personale())


  Nome: Tony
  Cognome: Stark
  Età: 40
  Residenza: Torre Stark

  Profilo:Insegnante
  Materie Insegnate:Ingegneria
  ***

  Nome: Mark
  Cognome: Zuckerberg
  Età: 33
  Residenza: California

  Profilo:Studente
  Corso Di Studi:SEO
  ***

Un altro caso d'uso in cui i metodi di classe si dimostrano utili, è qualora vogliate un Metodo che cambia comportamento in base alla sottoclasse che lo sta richiamando. Ad esempio, possiamo creare un metodo chiamato occupazione, che ci permette di identificare uno studente o un insegnante senza dover passare per la scheda personale.

class Persona:

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

    @classmethod
    def from_string(cls, stringa_persona, *args):
        nome, cognome, età, residenza = stringa_persona.split("-")
        return cls(nome, cognome, età, residenza, *args)

    # Esempio didattico: per ottenere lo stesso effetto con meno codice avremo potuto fare direttamente return cls.__nome__     
    @classmethod			    
    def occupazione(cls):
        if (cls.__name__ == "Studente"):
            return "Studente"
        else:
            return "Insegnante"
print(insg_uno.occupazione())
print(stud_uno.occupazione())

# output
Insegnante
Studente

Un terzo caso d'uso potrebbe essere l'utilizzo di un metodo di classe per modificare qualche proprietà della classe genitore, ad esempio potreste avere una variabile di classe chiamata istituto inizializzata con la stringa "Scuola Superiore", e creare un metodo di classe che vi consente quindi di modificarne il valore, assegnando ad esempio la stringa "Università".

Io vi ho dato l'idea, provate ora voi ad esercitarvi e a testare i limiti di questa tecnica!