Passa al contenuto principale

Classi, istanze e metodi

Conosci te stesso.

Massima delfica (Tempio di Apollo a Delfi)

Nella lezione precedente ti avevo chiesto di notare una cosa sola: i dati di un brano e ciò che sai farci vivono nello stesso blocco. Avevamo scritto una classe Brano e l’avevamo guardata da lontano, come si guarda un motore acceso senza aprire il cofano.

Oggi apriamo il cofano. Quella parola class, quel self che spuntava ovunque, quel __init__ dall’aria criptica: smettono di essere magia e diventano tre meccanismi precisi che, una volta capiti, userai per il resto del volume. E lo facciamo continuando proprio da Brano, dandogli più comportamenti e affiancandogli la sua compagna naturale: la Playlist di Risonanza.

Si riparte da Brano

Eccola di nuovo, la classe della scorsa lezione. Premi Run: non è cambiato niente, è il nostro punto di partenza.

Cinque righe di definizione, e dopo possiamo creare quanti brani vogliamo. Ma come funziona? Tre domande, tre sezioni: __init__, self, e la coppia attributi/metodi.

__init__: il costruttore

Quando scrivi Brano("Bohemian Rhapsody", "Queen", 354), stai chiedendo a Python di costruire un nuovo oggetto. Ma un oggetto appena nato è una scatola vuota: come fa a sapere che il suo titolo è Bohemian Rhapsody e la sua durata 354 secondi? Glielo dice il .

__init__ (doppio underscore, init, doppio underscore — i programmatori lo pronunciano dunder init) è un metodo speciale con un solo compito: inizializzare lo stato di un oggetto appena creato. Non lo chiami mai tu esplicitamente — è Python a invocarlo, in automatico, ogni volta che istanzi la classe.

Vediamolo all’opera. Ho infilato un print dentro __init__: comparirà esattamente una volta per ogni brano che creo.

Tre oggetti creati, tre stampe: __init__ è scattato tre volte, una per brano. Nessuno l’ha chiamato a mano — è il prezzo che Python paga in automatico ogni volta che apre la parentesi su Brano(...).

self: l’oggetto che conosce sé stesso

Hai notato che il primo parametro di __init__ — e, vedremo, di ogni metodo — è self? Eppure quando crei un brano passi tre argomenti, non quattro:

bohemian = Brano("Bohemian Rhapsody", "Queen", 354) # 3 argomenti, non 4

Dove finisce self? Non lo passi tu: lo passa Python. self è il riferimento all’oggetto specifico che si sta costruendo o usando in quel momento. Dentro __init__, quando scrivi self.titolo = titolo, stai dicendo: “attacca questo titolo a questo brano qui, non a un altro”.

È letteralmente l’oggetto che conosce sé stesso — da cui la citazione in cima. Ogni self.qualcosa è un dato che l’oggetto porta con sé e a cui può sempre accedere.

La prova che ogni oggetto ha il suo self, indipendente dagli altri:

Dentro riproduci, self è bohemian quando lo chiami su bohemian, ed è bury quando lo chiami su bury. Per questo il contatore di uno non tocca quello dell’altro: self.riproduzioni += 1 agisce sempre e solo sull’oggetto su cui hai chiamato il metodo.

In altri linguaggi (Java, C++, JavaScript) questo riferimento esiste ma è implicito e si chiama this. Python ha scelto di renderlo esplicito: explicit is better than implicit, recita lo Zen of Python. Discutibile, ma è così, e dopo un po’ ci si affeziona.

Attributi e metodi: ciò che un oggetto ha e ciò che sa fare

Ora abbiamo il vocabolario per dare un nome ai due tipi di cose che vivono dentro una classe.

  • Gli attributi sono le variabili attaccate a un oggetto: il suo stato, ciò che ha. Li crei nel costruttore con self.qualcosa = ... e ogni istanza ha la propria copia indipendente. In Brano sono titolo, artista, durata, riproduzioni.
  • I metodi sono le funzioni definite dentro la classe: il comportamento, ciò che l’oggetto sa fare. Sono normali funzioni con due particolarità — vivono nella classe e il loro primo parametro è self. In Brano per ora c’è riproduci.

A entrambi accedi con la dot notation, la notazione col punto: oggetto.attributo per lo stato, oggetto.metodo() per il comportamento. La differenza è nelle parentesi.

Modificare un attributo direttamente dall’esterno (bohemian.durata = 355) Python lo permette, ma è una libertà di cui più avanti impareremo a diffidare: significa che chiunque, da qualunque punto del programma, può alterare lo stato di un oggetto scavalcando ogni controllo. Per ora ci basta sapere che la sintassi esiste; il modo di mettere dei paletti si chiama incapsulamento, e ha una lezione tutta sua.

Un metodo che lavora lo stato

Un metodo non deve per forza cambiare l’oggetto: può anche limitarsi a leggerne lo stato e restituirne una versione più comoda. La durata di un brano la teniamo in secondi (354), ma a un essere umano «5:54» dice molto di più. Aggiungiamo un metodo che traduce:

durata_minuti non aggiunge nessun nuovo dato: pesca da self.durata — l’unico dato vero — e ne ricava una forma leggibile al momento della chiamata. Tieni a mente questa idea del “derivo invece di memorizzare”: tra poco, con la Playlist, vedremo cosa succede a chi la ignora.

Una seconda classe: la Playlist

Una classe sola fa una vita triste. Risonanza non gestisce un brano alla volta: li raccoglie in playlist. E una playlist è un’ottima candidata a diventare un oggetto — ha uno stato (un nome, un elenco di brani) e dei comportamenti (aggiungere un brano, riprodurli tutti).

Nota la cosa interessante: gli attributi di Playlist non sono solo numeri e stringhe. Uno di essi, brani, è una lista di oggetti Brano. Un oggetto può contenere altri oggetti — è un’idea potente, si chiama composizione, e merita la sua lezione; qui ci serve solo come secondo banco di prova per __init__, self, attributi e metodi.

Guarda riproduci_tutti: scorre self.brani e su ciascun elemento chiama brano.riproduci(). Un oggetto Playlist che fa lavorare i suoi oggetti Brano — esattamente la collaborazione tra entità di cui parlavamo nella prima lezione.

Predici l’output: l’attributo che si scollega

Ora la trappola promessa. Voglio sapere quanti brani contiene una playlist. Prima tentazione: lo salvo in un attributo numero_brani, lo parto da zero e lo incremento quando aggiungo. Senonché, scrivendo aggiungi, mi distraggo e lo dimentico.

Prima di premere Run, predici l’output: cosa stamperà l’ultima riga?

len(p.brani) dice 2, ma numero_brani è ancora fermo a 0. Avevo due fonti di verità per lo stesso fatto — la lista e il contatore — e bastava dimenticare un aggiornamento perché divergessero. È un bug subdolo (ha pure un nome da convegno: state desynchronization) ed è esattamente parente del bug silenzioso delle liste parallele della scorsa lezione.

La cura è la stessa idea di durata_minuti: non memorizzare ciò che puoi derivare. Il numero di brani è già dentro la lista — basta contarli quando serve.

numero_brani e durata_totale non possono più mentire: ogni volta ripartono dall’unica fonte di verità, la lista self.brani. Aggiungi un brano e i conti tornano da soli, senza che tu debba ricordarti nulla.

Disegnare una classe: prime notazioni UML

Quando i programmatori si scambiano idee sulle classi, non si copiano addosso il codice: disegnano. La notazione standard si chiama UML (Unified Modeling Language), e per le classi usa una scatola a tre scomparti: il nome in alto, gli attributi (lo stato) in mezzo, i metodi (il comportamento) in basso. Il + davanti significa “visibile dall’esterno” (pubblico).

Leggi la scatola e hai già in testa la classe: cosa sa di sé (lo scomparto centrale) e cosa sa fare (quello in basso). I metodi si riconoscono dalle parentesi, esattamente come nel codice. Una Playlist però contiene dei Brano: quel legame — la freccia che unisce le due scatole — è il cuore della lezione sulle relazioni UML, dove imparerai a disegnare anche il filo tra gli oggetti, non solo gli oggetti.

Mettiti alla provaDomanda 1 / 2

Scrivi bohemian = Brano("Bohemian Rhapsody", "Queen", 354). Passi tre argomenti, ma __init__ ha quattro parametri (self, titolo, artista, durata). Perché non è un errore?

Scegli la risposta corretta