Refactoring

Dove si ristruttura un programma funzionante.

… Ma c’è una certa differenza tra un programma che funziona e un buon programma. Se vogliamo imparare a programmare non dobbiamo accontentarci di un programma che funziona, dobbiamo affinare una certa sensibilità anche all’aspetto estetico. Non dobbiamo affezionarci troppo al nostro prodotto, ma cercare di migliorarlo.

Parametri di default

Il risultato del programma è soddisfacente e anche la distanza tra i mattoni sembra adeguata, ma se cambiamo il lato dei quadrati, andrà ancora bene? Parametrizziamo anche quella, ma lo facciamo dandole come valore predefinito 5:

def fila(lato, numero, spazio_colonne=5):
    """Disegna una fila di mattoni."""
    for cont_col in range(numero):
        quadrato(lato)
        tina.up()
        tina.forward(lato+spazio_colonne)
        tina.down()
    tina.up()
    tina.back(numero*(lato+spazio_colonne))
    tina.down()

In questo modo la funzione fila può essere chiamata con due o con tre argomenti:

  • Se viene chiamata con due argomenti, il terzo viene automaticamente posto uguale a 5,

    >>> fila(30, 3)
    

    Disegna una fila di 3 quadrati di lato 30 distanziati di 5 passi,

  • Se viene chiamata con tre argomenti, il terzo parametro assumerà il terzo valore.

    >>> fila(30, 3, 12)
    

    Disegna una fila di 3 quadrati di lato 30 distanziati di 12 passi.

In questo modo la funzione fila(lato, numero, spazio_colonne=5) è diventata più flessibile.

Anche la funzione muro() va cambiata, ci sono un paio di cose che stonano in questa funzione:

  • cinque righe si ripetono quasi identiche, e non va bene che in un programma si ripetano blocchi di istruzioni (quasi) identiche;
  • ci sono troppi numeri;

Partiamo dal primo problema: la soluzione è costruire una funzione che le esegua con un solo comando.

Le tre righe che vanno da tina.up() a tina.down() producono uno spostamento di Tartaruga in avanti e a sinistra, senza lasciare traccia. Possiamo generalizzare questo comportamento aggiungendo oltre allo spostamento verticale uno orizzontale. Possiamo costruire una funzione sposta che riceve come argomenti lo spostamento nella direzione della Tartaruga e lo spostamento nella sua direzione perpendicolare. Questa funzione dovrà avere quindi due parametri:

def sposta(avanti=0, sinistra=0):
    """Effettua uno spostamento di Tartaruga
       in avanti e verso sinistra senza disegnare la traccia."""
    tina.up()
    tina.forward(avanti)
    tina.left(90)
    tina.forward(sinistra)
    tina.right(90)
    tina.down()

Scrivendo i parametri in questo modo, possiamo chiamare la funzione sposta in vari modi:

  • sposta(), non fa niente;
  • sposta(47), sposta avanti Tartaruga di 47 unità senza tracciare segni;
  • sposta(47, 61), sposta Tartaruga avanti di 47 e a sinistra di 61 unità senza tracciare segni;
  • sposta(sinistra=61), sposta Tartaruga a sinistra di 61 unità senza tracciare segni;

Anche la funzione fila può essere migliorata usando questa funzione:

def fila(lato, numero, spazio_colonne=5):
    """Disegna una fila di mattoni quadrati."""
    for cont_col in range(numero):
        quadrato(lato)
        sposta(avanti=lato+spazio_colonne)
    sposta(avanti=-numero*(lato+spazio_colonne))

E muro diventa:

def muro():
    """Disegna un muro di quadrati."""
    for cont_rig in range(15):
        fila(20, 18,)
        sposta(sinistra=20+5)
    sposta(sinistra=-15*(20+5))

Nota

in queste funzioni ho utilizzato un terzo metodo per inserire un argomento in un parametro: il passaggio dell’argomento «per nome».

Confrontando la versione precedente di muro con questa si può notare una bella semplificazione!

Ora occupiamoci di eliminare un po” di numeri parametrizzando anche la procedura muro. I numeri presenti nella funzione riguardano: la lunghezza del lato, il numero di righe, il numero di colonne, lo spazio tra le righe, lo spazio tra le colonne. Questi due ultimi valori possono essere lasciati di default uguali a 5.

Trasformandoli tutti in parametri la funzione muro diventa:

def muro(lato, righe, colonne, spazio_colonne=5, spazio_righe=5):
    """Disegna un muro di mattoni."""
    for cont_rig in range(righe):
        fila(lato, colonne, spazio_colonne)
        sposta(sinistra=lato+spazio_righe)
    sposta(sinistra=-righe*(lato+spazio_righe))

E tutto il programma diventa:

# Lettura delle librerie
import pygraph.pyturtle as tg

# Definizione di funzioni

def sposta(avanti=0, sinistra=0):
    """Effettua uno spostamento orizzontale e verticale
       di Tartaruga senza disegnare la traccia."""
    tina.up()
    tina.forward(avanti)
    tina.left(90)
    tina.forward(sinistra)
    tina.right(90)
    tina.down()

def quadrato(lato):
    """Disegna un quadrato di dato lato vuoto o pieno."""
    for cont in range(4):
        tina.forward(lato)
        tina.left(90)

def fila(lato, numero, spazio_colonne=5):
    """Disegna una fila di mattoni quadrati."""
    for cont_col in range(numero):
        quadrato(lato)
        sposta(avanti=lato+spazio_colonne)
    sposta(avanti=-numero*(lato+spazio_colonne))

def muro(lato, righe, colonne, spazio_colonne=5, spazio_righe=5):
    """Disegna un muro di mattoni."""
    for cont_rig in range(righe):
        fila(lato, colonne, spazio_colonne)
        sposta(sinistra=lato+spazio_righe)
    sposta(sinistra=-righe*(lato+spazio_righe))

# Programma principale
piano = tg.TurtlePlane()
tina = tg.Turtle()

sposta(-250, -190)
muro(20, 15, 20)

# Rende attiva la finestra grafica
piano.mainloop()

Compattiamo il codice

Funziona tutto a meraviglia, o almeno dovrebbe funzionare se non ho introdotto qualche errore! Potremmo considerarci soddisfatti se l’istinto del programmatore non rodesse dentro… Perdendo un po” in chiarezza possiamo compattare di più il codice. Vale la pena? Dipende da ciò che si vuole ottenere, comunque, prima di dare un giudizio proviamo un’altra versione.

Prima avevamo staccato delle righe di codice per fare una funzione separata che realizzasse gli spostamenti data la componente orizzontale e verticale. Ora fondiamo due funzioni che hanno degli elementi in comune: la procedura muro e la procedura fila. L’intero muro si può ottenere annidando, uno dentro l’altro due cicli: il ciclo più esterno impila le file e quello più interno allinea quadrati. In pratica al posto della chiamata alla procedura fila(...), trascriviamo tutte le sue istruzioni. Dobbiamo anche aggiustare i nomi e l’indentazione:

def muro(lato, righe, colonne, spazio_colonne=5, spazio_righe=5):
    """Disegna un muro di mattoni."""
    for cont_rig in range(righe):                # disegna tutte le righe
        for cont_col in range(colonne):          # disegna una riga
            quadrato(lato)
            sposta(avanti=lato+spazio_colonne)
        sposta(avanti=-colonne*(lato+spazio_colonne))
        sposta(sinistra=lato+spazio_righe)
    sposta(sinistra=-righe*(lato+spazio_righe))

I due cicli annidati devono avere due variabili diverse noi abbiamo usato cont_rig e cont_col, ma spesso si usano per queste variabili di ciclo i nomi i e j.

Possiamo ancora eliminare una riga di codice! Come?

Alla fine del ciclo più interno ci sono due chiamate alla funzione sposta che possono essere sostituite da una sola.

Nota

Avere cicli annidati di solito non è una buona idea: di solito rende il codice meno comprensibile. Quindi è una scelta da fare consapevolmente.

Nota

Il fatto di ridurre le dimensioni di un programma non è solo una questione di spazio, ma è fondamentale per renderne più semplice la manutenzione.

E con questo il programma è terminato…

# Lettura delle librerie
import pygraph.pyturtle as tg

# Definizione di funzioni

def sposta(avanti=0, sinistra=0):
    """Effettua uno spostamento orizzontale e verticale
       di Tartaruga senza disegnare la traccia."""
    tina.up()
    tina.forward(avanti)
    tina.left(90)
    tina.forward(sinistra)
    tina.right(90)
    tina.down()

def quadrato(lato):
    """Disegna un quadrato di dato lato vuoto o pieno."""
    for cont in range(4):
        tina.forward(lato)
        tina.left(90)

def muro(lato, righe, colonne, spazio_righe=5, spazio_colonne=5):
    """Disegna un muro di mattoni."""
    for cont_rig in range(righe):                # disegna tutte le righe
        for cont_col in range(colonne):          # disegna una riga
            quadrato(lato)
            sposta(avanti=lato+spazio_colonne)
        sposta(-colonne*(lato+spazio_colonne), lato+spazio_righe)
    sposta(sinistra=-righe*(lato+spazio_righe))

# Programma principale
piano = tg.TurtlePlane()
tina = tg.Turtle()

sposta(-250, -190)
muro(20, 15, 20)

# Rende attiva la finestra grafica
piano.mainloop()

…o quasi…

Riassumendo

  • Quando è possibile è meglio sostituire i numeri e le costanti presenti in una funzione con parametri.
  • Le procedure possono avere anche dei parametri con dei valori predefiniti (di default).
  • Gli argomenti di una funzione possono essere passati anche per nome.
  • Più linee di istruzioni che si ripetono all’interno di un programma possono essere raggruppate in un’unica funzione.
  • All’interno di un ciclo possono essere annidati altri cicli, bisogna fare attenzione al nome delle variabili.
  • Un programma più breve, più semplice e con nomi significativi è meglio di un programma più lungo, più complicato e con nomi insensati.