Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Membri di Classe

Fino ad adesso, ogni attributo che abbiamo definito nelle nostre classi era un attributo di istanza: ogni oggetto aveva la sua copia, con il suo valore indipendente dagli altri oggetti della stessa classe.

Ma a volte ci sono informazioni che non appartengono a un singolo oggetto, bensì alla classe intera. Pensate ad esempio a un contatore: quante istanze di questa classe sono state create? Non ha senso chiederlo a un singolo oggetto, ha senso chiederlo alla classe.

Per gestire questi casi, Python mette a disposizione tre strumenti che andremo a vedere in questo capitolo:

  • le variabili di classe
  • i metodi di classe (@classmethod)
  • i metodi statici (@staticmethod)

Variabili di classe

Una variabile di istanza è definita dentro __init__ tramite self, e ogni oggetto ne ha una copia propria:

class Persona:
    def __init__(self, nome):
        self.nome = nome   # variabile di ISTANZA: ogni oggetto ha la sua

p1 = Persona("Gino")
p2 = Persona("Pina")
# p1.nome è "Gino", p2.nome è "Pina": copie indipendenti

Una variabile di classe viene invece definita dentro la classe ma fuori da qualsiasi metodo, e appartiene alla classe stessa: tutti gli oggetti la condividono e ne vedono lo stesso valore.

class Persona:
    numero_persone = 0     # variabile di CLASSE: condivisa fra tutti gli oggetti

    def __init__(self, nome):
        self.nome = nome
        Persona.numero_persone += 1   # si accede con il nome della classe, non con self

p1 = Persona("Gino")
p2 = Persona("Pina")
p3 = Persona("Gigi")

print(Persona.numero_persone)   # stampa 3

Notate la differenza di accesso: le variabili di istanza si leggono e scrivono tramite self, le variabili di classe tramite il nome della classe (Persona.numero_persone).

Note

Variabile di classe vs variabile di istanza

  • Una variabile di istanza (self.nome) appartiene al singolo oggetto. Ogni oggetto ha la sua copia.
  • Una variabile di classe (Classe.nome) appartiene alla classe. Tutti gli oggetti la condividono.

Metodi di Classe (@classmethod)

Un metodo normale riceve come primo parametro self, ovvero il riferimento all’oggetto su cui viene chiamato.

Un metodo di classe riceve invece come primo parametro cls, ovvero il riferimento alla classe stessa (non a un singolo oggetto). Si indica con il decoratore @classmethod.

Il caso d’uso più classico è il costruttore alternativo: quando vogliamo offrire più modi di creare un oggetto, ma Python permette un solo __init__, possiamo aggiungere dei @classmethod che costruiscono l’oggetto a partire da dati in un formato diverso.

Esempio: la classe Orario accetta ore, minuti e secondi. Ma vogliamo anche poter costruire un orario a partire da una stringa nel formato "HH:MM:SS":

class Orario:
    def __init__(self, ore, minuti, secondi):
        self.ore = ore
        self.minuti = minuti
        self.secondi = secondi

    def __str__(self):
        return f"{self.ore:02d}:{self.minuti:02d}:{self.secondi:02d}"

    @classmethod
    def da_stringa(cls, stringa):
        """Costruisce un Orario da una stringa nel formato HH:MM:SS"""
        parti = stringa.split(":")
        ore = int(parti[0])
        minuti = int(parti[1])
        secondi = int(parti[2])
        return cls(ore, minuti, secondi)   # cls(...) equivale a chiamare Orario(...)

# -------------------------------------------

if __name__ == "__main__":
    o1 = Orario(8, 30, 0)
    print(o1)              # 08:30:00

    o2 = Orario.da_stringa("14:45:22")
    print(o2)              # 14:45:22

cls(ore, minuti, secondi) chiama il costruttore della classe, esattamente come Orario(ore, minuti, secondi): scrivere cls invece del nome della classe è una buona abitudine perché, se in futuro si eredita la classe, cls si adatta automaticamente alla sottoclasse.

Note

self vs cls

  • selfmetodo di istanza: riceve l’oggetto su cui è chiamato. Può accedere agli attributi di istanza e di classe.
  • clsmetodo di classe (@classmethod): riceve la classe stessa. Può accedere e modificare variabili di classe, ma non ha accesso agli attributi di istanza.

Metodi Statici (@staticmethod)

Un metodo statico non riceve né selfcls: è una funzione che sta dentro la classe per motivi organizzativi (è concettualmente legata ad essa), ma non ha bisogno di accedere né all’istanza né alla classe stessa.

Si indica con il decoratore @staticmethod e si chiama tramite il nome della classe (o tramite un oggetto, anche se è meno leggibile).

L’utilizzo tipico è per funzioni di utilità e validazione legate al dominio della classe:

class Orario:
    def __init__(self, ore, minuti, secondi):
        self.ore = ore
        self.minuti = minuti
        self.secondi = secondi

    def __str__(self):
        return f"{self.ore:02d}:{self.minuti:02d}:{self.secondi:02d}"

    @staticmethod
    def formato_valido(stringa):
        """Ritorna True se la stringa è nel formato HH:MM:SS, False altrimenti"""
        parti = stringa.split(":")
        if len(parti) != 3:
            return False

        ore, minuti, secondi = int(parti[0]), int(parti[1]), int(parti[2])
        if 0 <= ore <= 23 and 0 <= minuti <= 59 and 0 <= secondi <= 59:
            return True
        return False

    @classmethod
    def da_stringa(cls, stringa):
        if not Orario.formato_valido(stringa):
            raise ValueError(f"Formato non valido: '{stringa}'. Usare HH:MM:SS")
        parti = stringa.split(":")
        return cls(int(parti[0]), int(parti[1]), int(parti[2]))

# -------------------------------------------

if __name__ == "__main__":
    print(Orario.formato_valido("14:45:22"))   # True
    print(Orario.formato_valido("25:00:00"))   # False (ore fuori range)
    print(Orario.formato_valido("pippo"))      # False

    o = Orario.da_stringa("09:15:30")
    print(o)      # 09:15:30

Tip

Quando usare cosa?

  • Metodo normale (self): quando devo accedere o modificare gli attributi di quell’istanza.
  • Metodo di classe (@classmethod, cls): quando devo accedere a variabili di classe o costruire un nuovo oggetto in modo alternativo.
  • Metodo statico (@staticmethod): quando la funzione è concettualmente legata alla classe ma non ha bisogno di accedere né all’istanza né alla classe.

Esercizio svolto: la classe Automobile con contatore

Definiamo la classe Automobile con un contatore di istanze (variabile di classe), un costruttore alternativo che accetta una stringa nel formato "marca:modello", e un metodo statico di validazione del formato.

class Automobile:
    numero_automobili = 0    # variabile di classe: conta le auto create

    def __init__(self, marca, modello):
        self.marca = marca
        self.modello = modello
        Automobile.numero_automobili += 1

    def __str__(self):
        return f"Automobile: {self.marca} {self.modello}"

    @classmethod
    def da_stringa(cls, stringa):
        """Crea un'Automobile da una stringa nel formato 'marca:modello'"""
        if not Automobile.formato_valido(stringa):
            raise ValueError(f"Formato non valido: '{stringa}'. Usare 'marca:modello'")
        parti = stringa.split(":")
        return cls(parti[0].strip(), parti[1].strip())

    @staticmethod
    def formato_valido(stringa):
        """Ritorna True se la stringa contiene esattamente un ':' con testo da entrambi i lati"""
        parti = stringa.split(":")
        return len(parti) == 2 and len(parti[0].strip()) > 0 and len(parti[1].strip()) > 0

# -------------------------------------------

if __name__ == "__main__":
    a1 = Automobile("Fiat", "Panda")
    a2 = Automobile("Renault", "Clio")
    a3 = Automobile.da_stringa("Ford : Fiesta")

    print(a1)
    print(a2)
    print(a3)
    print("Auto create:", Automobile.numero_automobili)   # 3

    # test del metodo statico
    print(Automobile.formato_valido("Toyota:Yaris"))   # True
    print(Automobile.formato_valido("pippo"))           # False

Output atteso:

Automobile: Fiat Panda
Automobile: Renault Clio
Automobile: Ford Fiesta
Auto create: 3
True
False

Esercizi di comprensione


Esercizio 1301

Definire la classe Libro con attributi titolo e autore. Aggiungere una variabile di classe numero_libri che tenga traccia di quanti oggetti Libro sono stati creati.

Creare almeno 4 libri e verificare che Libro.numero_libri riporti il valore corretto.


Esercizio 1302

Riprendere la classe Giocatore dell’esercizio 1111. Aggiungere una variabile di classe numero_giocatori che si incrementa ad ogni creazione di un nuovo giocatore.

Creare una squadra di 5 giocatori e verificare il contatore.


Esercizio 1303

Definire la classe Data con attributi giorno, mese, anno.

Aggiungere un @classmethod da_stringa(cls, stringa) che costruisce un oggetto Data a partire da una stringa nel formato "GG/MM/AAAA".

Aggiungere un @staticmethod formato_valido(stringa) che verifica se la stringa è nel formato corretto (solo controllo del formato, non della validità della data).

Testare la creazione di una data tramite il costruttore normale e tramite il classmethod. Testare il metodo statico con almeno una stringa valida e una non valida.


Esercizio 1304

Definire la classe ContoCorrente (riprendendo l’esercizio 1105) e aggiungere:

  • una variabile di classe numero_conti che conta i conti creati;
  • una variabile di classe totale_depositi che tiene il totale di tutti i versamenti eseguiti su tutti i conti (si aggiorna ogni volta che un deposito va a buon fine);
  • un @classmethod report(cls) che stampa il numero di conti aperti e il totale dei depositi complessivi.

Creare 3 conti, eseguire alcuni depositi e prelievi, poi chiamare ContoCorrente.report().


Esercizio 1305: DIFFICILE

Definire la classe Dado con attributo facce (numero di facce del dado, default 6). Aggiungere:

  • un @staticmethod lancia_dado(facce) che ritorna un numero casuale fra 1 e facce;
  • un metodo normale lancia(self) che usa il metodo statico per lanciare il dado dell’istanza;
  • un @classmethod dado_percentuale(cls) che crea e ritorna un dado da 100 facce;
  • un @classmethod dado_destino(cls) che crea e ritorna un dado da 20 facce.

Testare tutti i metodi e i costruttori alternativi.


Esercizio 1306

Definire la classe Orario completa come mostrata negli esempi del capitolo (attributi ore, minuti, secondi; __init__; __str__; formato_valido come @staticmethod; da_stringa come @classmethod).

Aggiungere inoltre:

  • il metodo normale in_secondi(self) che ritorna il numero di secondi trascorsi dalla mezzanotte;
  • un @classmethod da_secondi(cls, secondi_totali) che costruisce un Orario a partire dal numero di secondi trascorsi dalla mezzanotte.

Verificare che Orario.da_secondi(o.in_secondi()) ricostruisca correttamente l’orario originale per almeno 3 valori diversi.