Una nuova classe

Dove definiamo la nostra prima classe

Classi e oggetti

Riassumiamo quello che avviene quando si esegue il programma man1.py:

  1. viene caricata la libreria pyturtle.py;
  2. viene creato un oggetto della classe TurtlePlane() della libreria pyturtle.py;
  3. viene creata una tartaruga cioè un oggetto della classe Turtle della libreria pyturtle.py;
  4. vengono definite tre funzioni (sposta(), quadrato(), muro());
  5. viene eseguito il programma principale che sposta la tartaruga e chiama la funzione muro che, a sua volta, chiama le funzioni quadrato e sposta.

Nel programma precedente abbiamo già creato e utilizzato un oggetto della classe TurtlePlane e un oggetto della classe Turtle. Abbiamo crearto una tartaruga con l’istruzione:

tina = tg.Turtle()
  • tina è un nome (un identificatore) che ci siamo inventati noi;
  • al nome tina è associato un oggetto della classe Turtle presente nella libreria tg (che sta per pyturtle).

Quindi tina è un riferimento ad un oggetto della classe Turtle. Nella classe vengono definite le caratteristiche e i comportamenti degli oggetti appartenenti a quella classe.

In questo capitolo vogliamo creare una nuova classe di oggetti.

La libreria pyturtle definisce le classi TurtlePlane e Turtle, concentriamoci sulla seconda e, per semplicità, chiamiamo tartaruga un oggetto della classe Turtle.

Una classe può essere utilizzata per:

  1. costruire oggetti con le proprietà e i metodi di quella classe;
  2. costruire altre classi che ampliano quella classe.

Metodi

Ogni tartaruga è in grado di eseguire alcuni comandi,

sono i suoi metodi:

forward(<numero>), back(<numero>), …

I metodi sono funzioni che possono essere eseguite da tutti gli oggetti di quella classe. Per dire ad un oggetto di una classe di eseguire un suo metodo, devo scrivere il nome dell’oggetto seguito dal punto, dal nome del metodo e da una coppia di parentesi contenenti, eventualmente, gli argomenti necessari. In pratica se tina è un oggetto della classe Turtle l’istruzione:

tina.forward(97)

chiede all’oggetto collegato al nome tina di eseguire il suo metodo forward e 97 è l’argomento passato a questo metodo.

Quindi l’istruzione precedente comanda alla tartaruga tina di avanzare di 97 passi.

tp.reset()

comanda a tp di ripulire tutto il piano e di riportare la situazione allo stato iniziale. Perché ogni metodo deve essere preceduto dal nome dell’oggetto? Non sarebbe più semplice scrivere solo: forward(97) o reset()? Il fatto è che possiamo creare quanti oggetti vogliamo della classe Turtle quindi quando diamo un comando, dobbiamo specificare a quale oggetto quel comando è diretto:

tp = TurtlePlane()
tina = Turtle()
pina = Turtle()
gina = Turtle()
pina.left(120)
gina.left(240)
tina.forward(50)
pina.forward(100)
gina.forward(200)

In questo caso vengono create tre tartarughe, vengono sfasate di 120 gradi l’una dall’altra e infine vengono fatte avanzare di tre lunghezze diverse.

Attributi

Ogni tartaruga ha diverse caratteristiche proprie,
sono i suoi attributi: color, width, position, direction, …

Gli attributi definiscono lo stato di un oggetto. Ad esempio, per cambiare il colore della tartaruga e della sua penna si deve modificare il suo attributo color: color = <colore> dove <colore> è una stringa che contiene il nome di un colore, o color = (<rosso>, <verde>, <blu>) dove <rosso>, <verde>, <blu> sono tre numeri decimali compresi tra 0 e 1:

tina.color = "purple"

o

tina.color = (0.4, 0.4, 0.4)   # grigio

Viceversa se voglio memorizzare in una variabile l’attuale colore di una tartaruga potrò assegnare ad una variabile il valore di color:

colore_attuale_di_tina = tina.color

Note

per l’elenco completo di attributi e metodi si vedano i capitoli dedicati alle singole librerie.

Nuove classi

Una caratteristica importante delle classi è l’ereditarietà. Una classe può venir derivata da un’altra classe essere cioè figlia di un’altra classe; la classe figlia eredita dalla classe genitrice tutte gli attributi e i metodi e può:

  • aggiungere altri attributi,
  • aggiungere altri metodi,
  • modificare i metodi della classe genitrice.

Buona parte della programmazione OOP consiste nel progettare e realizzare classi e gerarchie di classi di oggetti. Realizzare una nuova classe può essere un lavoro molto complicato ma potrebbe essere anche molto semplice quando la classe che realizziamo estende qualche altra classe già funzionante.

Come esercizio proviamo a costruire la classe di una tartaruga che sappia costruire un muro. Come al solito, prima di mettere mano a grandi opere, iniziamo a lavorare ad un problema abbastanza semplice e conosciuto. Voglio avere una tartaruga che oltre a saper fare tutto quello che sanno fare le altre tartarughe sappia anche disegnare mattoni quadrati: la nuova tartaruga deve quindi estendere le capacità di Turtle. La sintassi che Python mette a disposizione per estendere la gerarchia di una classe è:

class <nome di una nuova classe>(<nome di una classe esistente>):

Per iniziare gli esperimenti avviamo IDLE, aprimao un nuovo file: menu-File-New File esalviamo questo file nella cartella dove mettiamo tutti i nostri lavori: menu-File-Save As.

Come sempre scriviamo un po’ di informazioni relative al programma e la docstring con una sintetica descrizione di cosa deve fare il programma stesso.

Poi:

  • carichiamo la libreria pyturtle;

  • definiamo una nuova classe derivata dalla classe tg.Turtle, una classe che, per ora, non fa niente (istruzione pass);

  • infine scriviamo il programma principale che:

    • crea un piano,
    • crea un Ingegnere,
    • cambia qualche attributo a questo ingegnere,
    • gli chiede di tracciare una linea in avanti di 100 passi,
    • rende attiva la finestra grafica.
# man2.py
# 3 gennaio 2018
# Daniele Zambelli

"""
Il programma deve creare una classe di oggetti
capaci di disegnare un muro di mattoni quadrati.
"""

# Lettura delle librerie
import pygraph.pyturtle as tg

# Definizione di classi
class Ingegnere(tg.Turtle):
    """Una tartaruga che sa costruire muri."""
    pass

# Programma principale
piano = tg.TurtlePlane()
leonardo = Ingegnere()
leonardo.color = 'red'
leonardo.width = 6
leonardo.forward(100)

# Rende attiva la finestra grafica
piano.mainloop()

Corretti eventuali errori di battitura possiamo osservare che la classe Ingegnere derivata dalla classe tg.Turtle e che contiene solo l’istruzione pass, si comporta esattamente come una tartaruga. Infatti leonardo è un oggetto della classe Ingegnere e Ingegnere è un discendente di tg.Turtle e quindi, già alla nascita, sa fare tutto quello che sa fare tg.Turtle.

Ora estendiamo le capacità di Ingegnere in modo che sappia disegnare mattoni quadrati.

Modifichiamo il programma sostituendo pass con la definizione del metodo quadrato e modificando anche il programma principale:

# Definizione di classi
class Ingegnere(tg.Turtle):
    """Una tartaruga che sa costruire muri."""
    def quadrato(lato):
        """Disegna un quadrato di dato lato."""
        for i in range(4):
            forward(lato)
            left(90)

# Programma principale
piano = tg.TurtlePlane()
leonardo = Ingegnere()
leonardo.quadrato(80)

ed eseguiamo il programma:

Traceback (most recent call last):
  File "/dati/.../prove/man2.py", line 25, in <module>
  leonardo.quadrato(80)
TypeError: quadrato() takes 1 positional argument but 2 were given

Accidenti, non va!!! E non solo non funziona, ma ci dà un errore decisamente assurdo: Python si lamenta che quadrato vuole un argomento e noi gliene avremmo passati due!? È strabico? Non sa contare?? È stupido??? Boh, mah, forse… Chi ha programmato questo linguaggio ha deciso che l’oggetto che deve eseguire un metodo viene passato come primo parametro del metodo stesso.

Note

Noi scriviamo:

leonardo.quadrato(80)

in realtà viene eseguito:

Ingegnere.quadrato(leonardo, 80)

provare per credere.

Quindi se quadrato è un metodo di una classe deve avere un primo parametro dentro il quale viene messo il riferimento all’oggetto che deve eseguire il metodo stesso. Per convenzione questo parametro viene chiamato self e noi seguiamo questa convenzione. Modifichiamo il metodo quadrato mettendo self come primo parametro:

class Ingegnere(Turtle):
    """Una tartaruga che sa costruire muri."""
    def quadrato(self, lato):
        """Disegna un quadrato di dato lato."""
        for i in range(4):
            forward(lato)
            left(90)

Ora quadrato ha i due parametri: uno per contenere l’oggetto che deve eseguire il metodo e uno per contenere la lunghezza del lato. Fatti questi cambiamenti eseguiamo il programma ottenendo:

Traceback (most recent call last):
File
  File "/dati/.../prove/man2.py", in <module>
    leonardo.quadrato(80)
  File "/dati/.../prove/man2.py", line 18, in quadrato
    forward(lato)
NameError: name 'forward' is not defined

Ancora qualcosa che non va… Eppure questa volta quadrato ha i due parametri richiesti! Infatti l’errore è cambiato: ci dice che non esiste un nome globale forward. Infatti forward è un metodo della classe Turtle e quindi può essere eseguito solo da un oggetto di questa classe. Ma dove lo trovo un oggetto della classe Turtle mentre sto definendo la mia nuova classe? Se osserviamo bene, la soluzione al precedente errore ce l’ha messo a disposizione, è proprio l’oggetto contenuto in self. Dobbiamo modificare la chiamata a forward scrivendo: self.forward(lato). Terzo tentativo:

class Ingegnere(Turtle):
    """Una tartaruga che sa costruire muri."""
    def quadrato(self, lato):
        """Disegna un quadrato di dato lato."""
        for i in range(4):
            self.forward(lato)
            self.left(90)

Ovviamente quello che facciamo per forward lo dobbiamo fare anche per left. Modifichiamo l’Ingegnere e proviamo ancora il metodo ricorretto.

Va!!! Ora abbiamo una classe Ingegnere che oltre a saper fare tutto quello che sanno fare tutte le Tartarughe sa anche disegnare quadrati.

Ora possiamo prendere le altre funzioni del programma man1.py e trasformarle in metodi aggiungendo il parametro self dove serve modificando opportunamente il programma principale. Di seguito riporto l’intero programma

# man2.py
# 3 gennaio 2018
# Daniele Zambelli

"""
Il programma deve creare una classe di oggetti
capaci di disegnare un muro di mattoni quadrati.
"""

# Lettura delle librerie
import pygraph.pyturtle as tg

# Definizione di classi
class Ingegnere(tg.Turtle):
    """Una tartaruga che sa costruire muri."""

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

    def quadrato(self, lato):
        """Disegna un quadrato di dato lato."""
        for i in range(4):
            self.forward(lato)
            self.left(90)

    def muro(self, lato, righe, colonne,
             spazio_righe=5, spazio_colonne=5):
        """Disegna un muro di mattoni quadrati."""
        for i in range(righe):
            for j in range(colonne):
                self.quadrato(lato)
                self.sposta(o=lato+spazio_colonne)
            self.sposta(o=-colonne*(lato+spazio_colonne),
                        v=lato+spazio_righe)
        self.sposta(v=-righe*(lato+spazio_righe))

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

# Rende attiva la finestra grafica
piano.mainloop()

Salviamo, eseguiamo, … correggiamo gli errori che inevitabilmente sono stati fatti, …, rieseguiamo…

A parte la complicazione del parametro self, possiamo vedere come Python ci permetta di definire nuove classi in modo estremamente semplice. Utilizzando l’ereditarietà, cioè scrivendo classi derivate da altre già realizzate da altri, possiamo ottenere, con poche righe di programma classi:

  • potenti, perché estendono altre classi;
  • sicure, perché le classi genitrici sono utilizzate da molti altri programmatori ed eventuali errori sono sicuramente già stati trovati e corretti;
  • ulteriormente estendibili, ma questo lo vedremo nel prossimo capitolo.

Riassumendo

  • Per definire una classe si usa il comando class <nome della classe>():

  • Per definire una classe discendente da un’altra si utilizza il comando:

    class <nome della classe figlia>(<nome della classe genitrice>):

  • Quando si scrive una classe discendente da un’altra basta scrivere i metodi che si aggiungono ai metodi della classe genitrice o che li sostituiscono.

  • Quando si definisce un metodo di una classe si deve mettere come primo parametro il nome di una variabile che conterrà l’oggetto stesso, di solito self.

  • All’interno di una classe, il parametro self permette di richiamare i metodi e le proprietà dell’oggetto stesso.