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
selfvscls
self→ metodo di istanza: riceve l’oggetto su cui è chiamato. Può accedere agli attributi di istanza e di classe.cls→ metodo 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é self né cls: è 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_contiche conta i conti creati; - una variabile di classe
totale_depositiche tiene il totale di tutti i versamenti eseguiti su tutti i conti (si aggiorna ogni volta che un deposito va a buon fine); - un
@classmethodreport(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
@staticmethodlancia_dado(facce)che ritorna un numero casuale fra 1 efacce; - un metodo normale
lancia(self)che usa il metodo statico per lanciare il dado dell’istanza; - un
@classmethoddado_percentuale(cls)che crea e ritorna un dado da 100 facce; - un
@classmethoddado_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
@classmethodda_secondi(cls, secondi_totali)che costruisce unOrarioa 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.