18: OOP IN PILLOLE

EREDITARIETA'

Una delle caratteristiche della programmazione ad oggetti (OOP) è la capacità di poter facilmente modificare gli oggetti, estendendone gli attributi ed i metodi. Nel linguaggio tecnico questa caratteristica è detta ereditarietà, perchè, a partire da un oggetto dato (genitore, nell'informatica si usa il termine inglese parent), si può costruirne un altro che "eredita" tutte le sue funzionalità, ma ne aggiunge di nuove (o ne modifica alcune).

Per il momento non abbiamo le conoscenze per usare compiutamente l'ereditarietà (cosa che richiederebbe l'uso di nuove keywords di Python ed una lunga trattazione). Possiamo però sfruttare una semplice possibilità propria del nostro linguaggio: quella di aggiungere nuovi attributi ad un oggetto semplicemente assegnandoli.

Per farvi capire l'importanza di questa tecnica riprendiamo il programma fantasmi.py (qui): notiamo che quando muoviamo i fantasmini essi hanno tutti la stessa velocità. Cosa succederebbe se volessimo muovere ogni fantasma indipendentemente dagli altri, ognuno con una sua velocità differente? Possiamo pensare a due risposte:

La seconda strada è proprio quella dell'ereditarietà: gli Sprite di pygame non hanno un attributo che rappresenta la velocità dell'immagine sullo schermo, ma noi possiamo aggiungerlo.

AGGIUNGERE UN ATTRIBUTO

NUOVO PROGRAMMA: fantasmi2.py
ESERCIZIO 18.1: Riprendete la prima versione del programma fantasmi.py (qui) e fate le seguenti modifiche:

    .   .   .
        # se l'evento e' il timer ...
        elif ev.type == TIMERSHOT:
            pygame.time.set_timer(TIMERSHOT, trandom.randrange(200, 2501))
            ghost = pygame.sprite.Sprite(all_ghosts)
            ghost.image = ghost_images[random.randrange(4)]
            ghost.rect = ghost.image.get_rect()
            maxy = screen.get_height() - ghost.rect.height - 9
            ghost.rect.topright = (-1, random.randrange(10, maxy)
            ghost.xvel = random.randrange(1, 6)

    for ghost in all_ghosts:
        ghost.rect.x += ghost.xvel
        if ghost.rect.left > screen.get_rect().right:
            ghost.kill()		
    .   .   .
   

Nella sezione in cui creiamo il fantasma abbiamo aggiunto la riga #39: essa aggiunge un nuovo attributo xvel allo Sprite ghost con una semplice assegnazione (notate la solita sintassi con il punto per indicare che xvel è un attributo di ghost). Più avanti, nella #42, leggiamo questo attributo e lo usiamo per muovere il fantasma (aggiungete anche le ultime due righe che eliminano i fantasmi che sono usciti dallo schermo). Ecco fatto, ora potete lanciare il programma ed ogni fantasma avrà la sua velocità.

La regola è semplice: possiamo aggiungere un attributo ad un oggetto semplicemente dandogli un nome ed assegnandogli un valore (usando la sintassi che abbiamo visto sopra); vale sempre la regola fondamentale di Python: l'assegnazione deve sempre precedere ogni tentativo di lettura dell'attributo. Per questo è consigliabile scriverla il più vicino possibile alla funzione costruttore, quando l'oggetto è appena stato creato.

Proviamo a sfruttare più a fondo questa nuova possibilità: vogliamo dare ai nostri fantasmini un movimento più fluttuante. Mentre si muovono da sinistra a destra devono anche oscillare in alto e in basso. Per fare questo abbiamo bisogno:

ESERCIZIO 18.2: Modificate il programma come indicato: ESERCIZIO 18.3: Ora i fantasmini dovrebbero avere un grazioso movimento oscillante. Provate a modificare i parametri del programma (velocità y e numero dei cicli) secondo i vostri gusti.
SOLUZIONI

La possibilità di modificare gli oggetti predefiniti, rendendoli adatti alle caratteristiche del nostro programma, è l'argomento decisivo che fa pendere la bilancia a favore dell'uso degli Sprite anzichè delle Surface e Rect separati. Come avevamo detto all'inizio del tutorial (nella Lezione 2), la OOP è nata proprio per permettere di avere tutti i dati di un oggetto complesso raggruppati in una sola variabile, e questo in Python è decisamente facile (almeno per gli attributi).

IMITIAMO I COSTRUTTORI

Vi ho già detto che la funzione costruttore, nella OOP, è la particolare funzione che crea un nuovo oggetto, e che (è una regola di Python), deve avere lo stesso nome dell'oggetto (ad es. ghost = Sprite()). Quando modifichiamo un oggetto con l'ereditarietà abbiamo la possibilità di definire per esso un nuovo costruttore.

Anche questa, purtroppo, è una cosa per il momento al di fuori della nostra portata; possiamo, però, considerare i nostri fantasmini come degli oggetti Sprite modificati, e riunire tutte le righe che creano un nuovo fantasma in una sola funzione, che imita così il ruolo di un costruttore.

ESERCIZIO 18.4: Modificate ancora il programma in questo modo: SOLUZIONI

Una prima domanda potrebbe essere questa: l'istruzione che "ricarica" il timer va inclusa o no nella funzione new_ghost()? A dire il vero la cosa è indifferente, perchè la creazione di un fantasma va sempre fatta insieme al caricamento del timer per il prossimo fantasma. Tuttavia, dal punto di vista della logica, direi che è meglio lasciarla nel programma principale: la regola generale è che le funzioni dovrebbero essere il più possibile autosufficienti, anche dal punto di vista semantico: se la funzione si chiama new_ghost() non dovrebbe occuparsi anche di ricaricare il timer, e dovrebbe usare il minimo possibile le variabili globali del programma.

Ma c'è probabilmente una domanda molto più significativa: in definitiva, quali vantaggi ci sono dopo aver fatto queste modifiche? Bene, dal punto di vista della compattezza del programma, nessuno (perchè avete dovuto scrivere qualche riga di più). Anche dal punto di vista della velocità abbiamo uno svantaggio, perchè la chiamata ad una funzione è più lenta dell'esecuzione diretta del codice (ma questo non è certo un problema sui computer di oggi).

E allora? Bene, tutta la programmazione moderna è orientata a favorire la modularità e leggibilità dei programmi: vi sarete senz'altro accorti che man mano che impariamo cose nuove le dimensioni dei nostri programmi crescono, ed il nostro unico programma principale che si occupa di tutto diventa sempre meno gestibile.

E' quindi consigliato dividere i programmi molto lunghi in più parti che si occupano di cose diverse (e che possono essere raggruppate in files diversi). Nel nostro caso abbiamo mantenuto nel programma principale solo la chiamata alla funzione new_ghost(), facendo capire anche a chi lo legge per la prima volta che in quel punto viene generato un nuovo fantasma. I dettagli della creazione di un fantasma sono stati invece spostati nella funzione.

Fine della lezione