Passa al contenuto principale

Perché gli oggetti?

La frase più pericolosa in assoluto è: «L’abbiamo sempre fatto così.»

Grace Hopper

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 come brano["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:

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()

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

Mettiti alla provaDomanda 1 / 2

Nel codice procedurale con liste parallele, ordini una sola delle liste (per esempio durate). Qual è il rischio principale?

Scegli la risposta corretta