Come modificare il comportamento di una classe

Dove rompiamo un po’ gli schemi e costruiamo un metodo che oscura un metodo del genitore.

Estendere una classe: spostamento casuale

Nel capitolo precedente abbiamo realizzato un muro di quadrati. Tutti in ordine ben allineati, tutti uguali… Un po’ troppo in ordine, un po’ troppo uguali… Proviamo a mettere un po’ di disordine nello schema. Invece che disegnare un quadrato con il primo lato in direzione della tartaruga, possiamo fare in maniera che il quadrato sia spostato casualmente. Sorge subito un problema: Python non ha un comando per ottenere dei valori casuali. Niente paura, c’è una libreria che ci fornisce la funzione adatta. La libreria è random e la funzione che ci interessa è randrange(<numero>) che restituisce un numero intero compreso tra zero incluso e <numero> escluso.

Possiamo ripensare la procedura quadrato in questo modo:

definisco quadrato(lato) così:
  metto nella variabile angolo un numero casuale tra 0 e 30
  metto nella variabile spostamento un numero casuale tra 0 e 30
  ruoto tartaruga di angolo e la sposto di spostamento
  disegno il quadrato
  rimetto a posto tartaruga

Ora se utilizzassimo la programmazione classica dovremmo prendere il programma scritto precedentemente e modificare la procedura quadrato. La programmazione ad oggetti ci permette un meccanismo diverso:

  • si crea una classe discendente della classe Ingegnere,
  • si ridefinisce solo il metodo quadrato(...),

La nuova classe così costruita possiede tutte le caratteristiche della vecchia classe ma con il metodo quadrato diverso

Per poter realizzare questa nuova classe è necessario importare la libreria random e il man2.py che contiene la classe Ingegnere. Proviamola:

# man3.py
# 3 gennaio 2018
# Daniele Zambelli

"""
Il programma deve creare una classe di oggetti
capaci di disegnare un muro di mattoni quadrati
Disposti in posizione parzialmente casuale.
"""

# Lettura delle librerie
import pygraph.pyturtle as tg
import man2
import random

# Definizione di classi
class Architetto(man2.Ingegnere):
    """Una tartaruga che sa costruire muri scombinati."""

    def quadrato(self, lato):
        """Disegna un mattone spostato rispetto alla posizione
        attuale di Tartaruga."""
        angolo = random.randrange(30)
        spostamento = random.randrange(30)
        self.up()
        self.right(angolo)
        self.forward(spostamento)
        self.down()
        man2.Ingegnere.quadrato(self, lato)
        self.up()
        self.back(spostamento); self.left(angolo)
        self.down()

# Programma principale
piano = tg.TurtlePlane()
michelangelo = Architetto()
michelangelo.sposta(-250, -190)
michelangelo.muro(20, 15, 20)

# Rende attiva la finestra grafica
piano.mainloop()

Funziona… e non funziona.

Funziona perché abbiamo ottenuto i quadrati scombinati, come volevamo. Ma perché in un’altra finestra vengono disegnati anche i quadrati perfettamente schierati? Il fatto è che quando viene letta la libreria man2.py, questa viene anche eseguita, quindi, in particolare, vengono eseguite le sue ultime tre righe che producono il disegno dei quadrati tutti diritti. Python mette a disposizione gli strumenti (semplici) per evitare questo meccanismo.

Apriamo il programma man2.py e cambiamo il programma principale nel seguente modo:

# Programma principale
if __name__ == '__main__':
    piano = tg.TurtlePlane()
    leonardo = Ingegnere()
    leonardo.sposta(-250, -190)
    leonardo.muro(20, 15, 20)

    # Rende attiva la finestra grafica
    piano.mainloop()

Che più o meno vuol dire:

Solo se questo programma viene eseguito come programma principale esegui

le istruzioni del blocco che segue, atrimenti non fare niente.

L’effetto ottenuto pare abbastanza naturale, ma cosa avviene nell’interprete?

michelangelo è un oggetto della classe Architetto.

Il comando michelangelo.sposta(-250, -180) chiama il metodo sposta di Ingegnere il quale chiama il metodo forward di Turtle

Il comando michelangelo.muro(20, 15, 20) chiama il metodo muro di Ingegnere e questo chiama il metodo quadrato di Architetto e il metodo sposta di Ingegnere.

Questo comportamento non è semplice per il computer, ma appare naturale per il programmatore. Questo meccanismo permette di estendere a piacere librerie senza doverle modificare. In questo modo si possono realizzare librerie stabili, solide perché condivise e provate da molti utilizzatori, ma adattabili alle proprie esigenze.

Estendere una classe: aggiungiamo i colori

E se mi fossi stancato del bianco e nero e volessi dei mattoni colorati? Probabilmente la cosa più semplice potrebbe essere quella di modificare il metodo quadrato, ma proviamo a utilizzare ancora l’ereditarietà. Chiudiamo questo programma e apriamo una nuova finestra di editor dove definiamo una nuova classe, chiamiamola Artista, discendente da Architetto.

Dato che vogliamo usare man3.py come libreria dobbiamo modificare anche il suo programma principale in questo modo:

if __name__ == '__main__':
    # Programma principale
    piano = tg.TurtlePlane()
    michelangelo = Architetto()
    michelangelo.sposta(-250, -190)
    michelangelo.muro(20, 15, 20)

    # Rende attiva la finestra grafica
    piano.mainloop()

Effettuata la modifica e assicuratici di non aver introdotto errori eseguendo il programma, chiudiamo questo file e apriamone un altro dove mettiamo le solite intestazioni e una docstring.

Prima di procedere dobbiamo spendere due parole su uno dei modi di gestione dei colori. L’attributo color di Tartaruga accetta diversi tipi di argomenti: Ci sono vari modi per definire il colore di una tartaruga:

<tartaruga>.color = <nome di un colore>

ad esempio:

tina.color = 'pink'

oppure:

<tartaruga>.color = (<red>, <green>, <blue>)

dove <red>, <green>, e <blue> sono dei numeri razionali compresi tra 0 e 1 che rappresentano l’intensità dei tre colori fondamentali rosso, verde e blu. Ad esempio:

tina.color = (0.5, 0, 0.5) # metà rosso e metà blu

Ora useremo quesrto secondo metodo. Per riempire di un colore una figura si usa il metodo fill(0|1). Quando viene chiamato il metodo fill(1), Tartaruga tiene nota delle parti da riempire, quando viene chiamato fill(0), Tartaruga le riempie. Quindi, se si vuole colorare una figura, si deve chiamare il metodo fill con l’argomento uguale a 1 prima di iniziare a disegnarla e fill con l’argomento uguale a 0 alla fine. A questo punto la figura viene riempita con il colore attuale di Tartaruga.

Dovremo quindi creare una classe derivata da Architetto che ridefinisce il metodo quadrato. Questo metodo dovrà:

  • scegliere un colore a caso;
  • attivare il comando di riempimento;
  • disegnare il quadrato;
  • riempirlo con il colore scelto.

Ovviamente per la parte del disegno del quadrato potremo fare riferimento al metodo già definito in Architetto:

Dobbiamo quindi riscrivere il metodo quadrato:

# man4.py
# 3 gennaio 2018
# Daniele Zambelli

"""
Il programma deve creare una classe di oggetti
capaci di disegnare un muro di mattoni quadrati
Disposti in posizione parzialmente casuale e
con colori casuali.
"""

# Lettura delle librerie
import pygraph.pyturtle as tg
import man3
import random

# Definizione di classi
class Artista(man3.Architetto):
    """Una tartaruga che sa costruire muri scombinati e colorati."""

    def quadrato(self, lato):
        """Disegna un mattone scombinato e colorato."""
        self.color = (random.random(), random.random(), random.random())
        self.fill(1)
        man3.Architetto.quadrato(self, lato)
        self.fill(0)

if __name__ == '__main__':
    # Programma principale
    piano = tg.TurtlePlane()
    raffaello = Artista()
    raffaello.sposta(-250, -190)
    raffaello.muro(20, 15, 20)

    # Rende attiva la finestra grafica
    piano.mainloop()

Per vedere se hai capito bene questi meccanismi, prova a spegare a chi ai in fianco il significato del comando:

random.random()

e del comando:

man3.Architetto.quadrato(self, lato)

Non preoccuparti se non ci riesci, non sono concetti semplici e si chiariranno con l’uso.

Ma se ci riesci allora hai capito i concetti di base della Programmazione Orientata agli Oggetti (OOP)!

Riassumendo

  • Una classe discendente di un’altra, può aggiungere dei metodi alla classe genitrice.

  • Una classe discendente di un’altra, può anche modificarne il comportamento ridefinendo alcuni suoi metodi.

  • Per ridefinire un metodo basta definirne uno con lo stesso nome.

  • È l’interprete che si incarica di eseguire i metodi corretti tra tutti quelli che hanno lo stesso nome.

  • Una classe può forzare l’interprete ad eseguire un metodo di una sua classe antenata anteponendo al nome del metodo il nome della classe. Ad esempio il metodo quadrato di Artista chiama il metodo quadrato della classe Architetto in questo modo:

    Architetto.quadrato(self, lato)