Perché gli oggetti?
La frase più pericolosa in assoluto è: «L’abbiamo sempre fatto così.»
Nel primo volume hai imparato a dare istruzioni: variabili, condizioni, cicli, funzioni. Sai dire alla macchina cosa fare, passo dopo passo. È un superpotere, e per moltissimi problemi è esattamente l’approccio giusto: a uno script di trenta righe che converte un file non serve nessuna cerimonia in più.
In questo volume cambiamo mestiere. Smettiamo di chiederci soltanto «quali passi» e iniziamo a chiederci «quali cose esistono nel mio problema, cosa sanno fare, come collaborano». Per scoprire perché ne vale la pena, non partiremo dalla teoria: partiremo costruendo qualcosa e guardandolo rompersi.
Quel qualcosa è Risonanza, un piccolo lettore musicale da terminale che ci accompagnerà per buona parte del volume. Oggi ha tre canzoni. Tra poco ne avrà trecento, e proprio lì il nostro codice procedurale comincerà a scricchiolare.
Un caso concreto: la libreria di Risonanza
Vogliamo rappresentare i brani della nostra libreria. Con gli strumenti del Volume 1, la mossa naturale è una variabile per ogni caratteristica. Tre brani, quattro informazioni a testa (titolo, artista, durata in secondi, numero di riproduzioni): le teniamo in liste parallele, cioè liste diverse dove la posizione i descrive sempre lo stesso brano.
Funziona. Premi Run e vedrai i brani riprodotti e il contatore aggiornato. Con tre canzoni va tutto liscio: l’indice 0 è sempre Blinding Lights, in tutte e quattro le liste. Il dato e il suo significato stanno in piedi grazie a un patto implicito — “la posizione i è sempre lo stesso brano” — che per ora rispettiamo con disciplina.
Il problema dei patti impliciti è che reggono finché qualcuno si ricorda di rispettarli.
Quando la collezione cresce
Arriva la prima richiesta: aggiungere l’anno di uscita a ogni brano. Con le liste parallele significa creare una quinta lista, anni = [...], e tenerla allineata alle altre quattro. Poi arriva un brano nuovo: per aggiungerlo devi ricordarti di fare append su tutte e cinque le liste, nello stesso ordine. Ne dimentichi una?
titoli.append("As It Was")
artisti.append("Harry Styles")
durate.append(167)
# ...e qui ti squilla il telefono. Riproduzioni e anni non li aggiorni.
Da questo momento la posizione i non descrive più lo stesso brano in tutte le liste. Il patto è rotto, e Python non se ne accorge: continua a indicizzare felice, restituendoti dati di un brano mescolati con quelli di un altro.
Vediamolo in azione con un caso ancora più insidioso. Vogliamo ordinare i brani dal più lungo al più corto. Prima di premere Run, prova a predire l’output: cosa stamperà questo codice?
Hai predetto durate ordinate accanto ai titoli giusti? Quello che ottieni è Blinding Lights che dura 354 secondi (sono quasi sei minuti: chiunque l’abbia ascoltata sa che non è vero) e Bohemian Rhapsody sbrigata in 200. Abbiamo riordinato durate ma non titoli: gli indici non si corrispondono più, e ogni titolo si ritrova accanto alla durata di un altro.
Il problema di fondo è che le informazioni di un singolo brano sono sparse su strutture diverse, tenute insieme solo da un indice numerico e dalla nostra buona memoria. Più la libreria cresce, più liste e più funzioni dobbiamo tenere sincronizzate a mano. È il labirinto da cui vogliamo uscire.
I dizionari aiutano… ma non bastano
Hai già uno strumento del Volume 1 che migliora le cose: il dizionario. Invece di spargere i campi di un brano su quattro liste, li raccogliamo in un’unica struttura con chiavi parlanti.
Questo è un passo avanti vero: ora i dati di un brano viaggiano insieme in un solo oggetto. Una lista di dizionari la puoi ordinare senza disallineare niente, perché ogni dizionario si porta dietro tutti i suoi campi. Il bug della sezione precedente, qui, sparisce.
E allora basta così, mettiamo tutto in dizionari e andiamo al mare? Non proprio. Restano due crepe:
- Il comportamento abita altrove. I dati del brano stanno nel dizionario, ma le azioni che lo riguardano — riprodurlo, calcolare la durata in minuti, sapere se è un tormentone — vivono in funzioni separate, sparse nel file. Dati di qua, comportamento di là: il legame torna a essere implicito.
- Niente difende la coerenza. Nessuno ti impedisce di scrivere
brano["durata"] = -5(un brano di durata negativa) o di fare un refuso comebrano["titlo"], che Python accetta in silenzio creando una chiave nuova e sbagliata. Il dizionario è un contenitore neutro: non sa cosa significhi essere un brano valido, quindi non può proteggerti.
Ci serve qualcosa che faccia due cose insieme: tenere i dati di un brano e i comportamenti che li riguardano nello stesso posto, e dare a quel “posto” un’identità — questo è un brano, con le sue regole.
L’idea: tenere insieme dati e comportamento
Eccola, l’idea-madre di tutto il volume. Tienila a mente, perché incapsulamento, ereditarietà, polimorfismo e pattern non sono altro che conseguenze e strumenti di questa frase:
L’analogia classica è quella dei biscotti. La classe è lo stampo per i biscotti: definisce la forma “brano” — avrà un titolo, un artista, una durata, e saprà essere riprodotto. Gli oggetti sono i biscotti che sforni: Bohemian Rhapsody, Blinding Lights, Bury a Friend. Ognuno è un brano, ma è un’entità separata con i suoi valori. Lo stampo esiste una volta sola nel codice; gli oggetti possono essere centinaia.
Guarda come diventa la nostra rappresentazione di un brano. Non spaventarti per la parola class, per self o per quel __init__ dall’aria criptica: la meccanica è il cuore della prossima lezione. Qui voglio che tu noti una cosa sola — i dati del brano e ciò che sai farci vivono nello stesso blocco.
Nessuna lista parallela. Nessun indice da tenere allineato. Il titolo, la durata e il contatore di riproduzioni sono attaccati a bohemian, e l’azione riproduci() agisce esattamente su quel brano. Se domani crei un secondo brano, avrà il suo stato indipendente — sforniamo lo stampo una volta, sforniamo quanti biscotti vogliamo.
Possiamo disegnare lo stampo Brano con uno schema — è il primo assaggio di UML, la notazione con cui i programmatori si scambiano disegni di classi (la useremo spesso in questo volume):
Lo scomparto in alto elenca i dati (lo stato: titolo, artista, durata, riproduzioni); quello in basso elenca i comportamenti (le azioni: riproduci()). Il punto non è la grafica: è che entrambi stanno nella stessa scatola. Quella scatola è la classe — l’idea-madre resa visibile.
E il bug del disallineamento? Semplicemente non può più capitare, perché ogni brano si porta dietro tutti i suoi campi, incollati dentro lo stesso oggetto:
Riordina pure questa lista come vuoi: titolo e durata viaggiano insieme dentro l’oggetto, non c’è un secondo elenco da tenere sincronizzato. Il patto implicito è diventato una struttura esplicita.
Procedurale contro oggetti, fianco a fianco
Lo stesso compito — riprodurre un brano e contarne gli ascolti — nei due mondi:
- A oggetti
- Procedurale
class Brano:
def __init__(self, titolo, artista):
self.titolo = titolo
self.artista = artista
self.riproduzioni = 0
def riproduci(self):
self.riproduzioni += 1
# Dati e comportamento nello stesso posto
brano = Brano("Bury a Friend", "Billie Eilish")
brano.riproduci()
titoli = ["Bury a Friend"]
artisti = ["Billie Eilish"]
riproduzioni = [0]
# Il comportamento vive lontano dai dati,
# tenuto insieme solo dall'indice
def riproduci(indice):
riproduzioni[indice] += 1
riproduci(0)
Non è una questione di righe in meno (a volte la versione a oggetti è perfino più lunga). È una questione di dove vivono le cose: nel mondo a oggetti, se devi capire come funziona un brano, sai esattamente dove guardare. Tutto ciò che lo riguarda è dentro la sua classe.
Cosa ci portiamo dietro
Nel codice procedurale con liste parallele, ordini una sola delle liste (per esempio durate). Qual è il rischio principale?
Qual è l’idea centrale della programmazione a oggetti?