Metodi di classe e statici
Namespaces are one honking great idea — let’s do more of those!
Alla fine della scorsa lezione ti ho lasciato con una domanda apparentemente innocua: quanti brani ho creato in totale? Non quanti ne contiene una certa playlist — quelli li sai già contare — ma quanti oggetti Brano sono nati da quando il programma è partito. È una domanda strana, perché la risposta non appartiene a nessun brano in particolare: Bohemian Rhapsody non sa nulla di Blinding Lights, e non deve saperlo.
Finora ogni cosa che abbiamo scritto viveva dentro un oggetto, raggiunta tramite self. Oggi scopriamo che esiste un altro piano: ci sono dati e comportamenti che appartengono allo stampo — alla classe stessa — e non al singolo biscotto sfornato. È un piano che userai meno spesso di self, ma quando serve non c’è alternativa pulita.
La domanda che nessun brano sa rispondere
Proviamo la strada ingenua. Voglio un contatore dei brani creati: lo metto in una variabile e lo incremento dentro __init__. Dove? Fuori dalla classe, come variabile globale — sembra l’unico posto possibile.
Funziona, ma puzza. Quel contatore_brani è un’informazione su Brano che vive lontana da Brano, in una variabile globale che chiunque, da qualunque punto del file, può azzerare o falsare. È esattamente la malattia procedurale della prima lezione — stato scollegato dall’entità a cui appartiene — e quella parola chiave global è la spia rossa che ce lo conferma.
Il numero di brani creati è un fatto che riguarda lo stampo Brano. Quindi dovrebbe vivere sullo stampo.
Attributi di classe: lo stato dello stampo
Un attributo di classe si dichiara nel corpo della classe, direttamente — non dentro __init__, non tramite self. Ne esiste una sola copia, condivisa da tutte le istanze e dalla classe stessa.
Niente più variabile globale, niente più global. Il contatore vive dove deve: attaccato a Brano, e lo leggi con Brano.creati. Guarda la differenza tra le due righe dentro __init__:
Brano.creati += 1tocca l’attributo di classe: lo stampo, condiviso. Conta tutti i brani.self.riproduzioni = 0crea un attributo di istanza: una copia privata per questo brano. Ogni oggetto ha il suo contatore di ascolti, indipendente dagli altri.
Per un contatore, la condivisione è esattamente ciò che vuoi: un solo numero per tutti. Lo stesso vale per le costanti della classe — un valore uguale per ogni oggetto, come un limite massimo o un’etichetta di default. Ma la condivisione è un’arma a doppio taglio.
Quando la condivisione ti si ritorce contro
Vogliamo dare a ogni brano una lista di tag (i generi: “rock”, “pop”…). Prima tentazione: la dichiaro come attributo di classe, una tag = [] lì in alto, e ci appendo le etichette. Prima di premere Run, predici l’output: cosa stamperanno le ultime due righe?
Sorpresa: tutti e due i brani mostrano ['rock', 'electropop']. Avevi previsto una lista pulita per ciascuno? E invece tag è una sola lista, appesa allo stampo e condivisa da ogni brano: l’etichetta aggiunta a Bohemian compare anche su Bury, perché stanno scrivendo sullo stesso oggetto. È il contatore che ci serviva prima, ma questa volta la condivisione è un disastro.
La cura: una lista che deve essere propria di ogni oggetto va creata in __init__, come attributo di istanza.
Ora ogni brano si tiene i suoi tag. La regola pratica è semplice: dato che deve essere uguale e condiviso da tutti (un contatore, una costante) → attributo di classe; dato proprio di ogni oggetto, soprattutto se è una lista o un dizionario → attributo di istanza, in __init__.
Metodi di classe: @classmethod
Abbiamo messo dei dati sullo stampo. Ora mettiamoci dei metodi. Un metodo di classe non riceve un’istanza (self), ma la classe stessa, per convenzione chiamata cls. Lo dichiari premettendo il @classmethod.
A cosa serve un metodo che riceve la classe invece di un oggetto? Al suo uso più prezioso: costruire oggetti in modi alternativi.
I nostri brani, nel mondo reale, non arrivano sempre come tre argomenti ordinati. Spesso arrivano da un file, una riga per brano, con i campi separati da un punto e virgola: "Bohemian Rhapsody;Queen;354". Vogliamo poter creare un Brano direttamente da una riga così. La soluzione pulita è un costruttore alternativo: un metodo di classe che prende la riga, la smonta e restituisce un oggetto bell’e fatto.
Tre cose da notare:
da_rigala chiami sulla classe (Brano.da_riga(...)), non su un oggetto: è normale, perché l’oggetto ancora non esiste — è proprio lei a crearlo.clsè la classeBrano. Scriverereturn cls(...)significa “chiama il costruttore normale”, cioè__init__, con gli argomenti giusti. Il costruttore alternativo non scavalca__init__: lo usa, dopo aver preparato i dati.- Il contatore
Brano.creatiscatta lo stesso, perché alla finecls(...)passa comunque da__init__.
E le strade per arrivare a un brano possono essere più d’una. Mettiamo che certi dati arrivino invece come dizionario (è quello che otterresti leggendo un file JSON). Aggiungiamo un secondo costruttore alternativo, e guarda come due ingressi diversi producono lo stesso tipo di oggetto:
Tre strade — il costruttore normale Brano(...), da_riga, da_dizionario — un solo tipo di destinazione. Ognuna sa parlare un dialetto diverso (argomenti sciolti, testo, dizionario) e li traduce tutti nella stessa, unica forma canonica definita da __init__.
Metodi statici: @staticmethod
Resta un terzo caso, il più umile. A volte hai una funzione che è logicamente legata a una classe — parla del suo tema — ma non ha bisogno né di un oggetto specifico (self) né della classe (cls). È solo una funzione di supporto che ha senso tenere lì, vicino a chi la usa. Si dichiara con @staticmethod e non riceve né self né cls.
Ricordi durata_minuti della scorsa lezione? Era un metodo d’istanza, perché pescava da self.durata. Ma l’operazione “trasforma un numero di secondi in minuti:secondi” non ha bisogno di un brano specifico: dato un numero qualsiasi, restituisce la stringa. È un candidato perfetto a metodo statico — e così lo posso riusare ovunque, anche per la durata totale di una playlist, senza avere per forza un Brano sotto mano.
formatta_durata prende solo secondi: niente self, niente cls. Vive sotto Brano perché parla di durate di brani, ed è proprio questo il senso della citazione in cima alla lezione: una classe è anche uno spazio dei nomi (namespace), un cassetto dove raccogli ciò che logicamente sta insieme. Tenere formatta_durata accanto a Brano la mette dove chi legge il codice andrà a cercarla.
I tre tipi, fianco a fianco
Tre modi di scrivere un metodo dentro una classe, tre scopi diversi. La domanda da farsi è sempre la stessa: di cosa ha bisogno questo metodo per fare il suo lavoro?
| Tipo | Cosa riceve | Lo usi quando… | Come lo chiami |
|---|---|---|---|
| Metodo d’istanza | self (l’oggetto) | ti serve lo stato del singolo oggetto | oggetto.metodo() |
Metodo di classe (@classmethod) | cls (la classe) | ti serve la classe, o vuoi creare istanze (costruttori alternativi) | Classe.metodo() |
Metodo statico (@staticmethod) | né self né cls | è una funzione di supporto che appartiene al tema della classe | Classe.metodo() |
In pratica, scorri le domande in ordine:
- Ti serve lo stato del singolo oggetto (
self.qualcosa)? → metodo d’istanza, il caso normale. - Non l’oggetto, ma ti serve la classe — tipicamente per costruire istanze in modo alternativo? →
@classmethodconcls. - Non ti serve né l’uno né l’altra, ma la funzione ha senso vicino alla classe? →
@staticmethod. E se non ha senso nemmeno quello, è una semplice funzione del modulo.
Anche l’UML sa distinguere questi due piani: i membri che appartengono alla classe (l’attributo creati, i metodi da_riga e formatta_durata) si disegnano sottolineati, per dire “questo sta sullo stampo, non sul singolo oggetto”.
I tre membri sottolineati (il rendering aggiunge una linea sotto creati, da_riga e formatta_durata) vivono sulla classe; gli altri sul singolo brano. Una scatola sola, due piani di lettura.
Vuoi che ogni Brano abbia la propria lista di tag. Dichiari tag = [] nel corpo della classe, non dentro __init__. Cosa succede?
Il costruttore alternativo da_riga è un @classmethod che fa return cls(...). Perché non scriverlo come una normale funzione che fa return Brano(...)?
Ti serve una funzione che converte un numero di secondi in "minuti:secondi". Non usa nessun brano specifico né la classe, ma parla chiaramente di durate di brani. Come la scrivi?