Software in pratica

 ☗   ◀   ☰   ▶ 

UNO SGUARDO AGLI OGGETTI

Con le liste, e le altre cose incontrate fino qui, avete in mano i principali strumenti per descrivere ogni tipo di algoritmo (procedimento) il resto sono solo approfondimenti, ed evoluzioni nel modo di strutturare i programmi. Tra queste evoluzioni vi è la famosa programmazione ad oggetti (OOP), e Python come molti altri linguaggi moderni è totalmente orientato agli oggetti (object oriented).

Senza saperlo avete già incontrato almeno un oggetto, la lista, riconoscibile come tale dal fatto che dispone di funzioni interne (metodi) in grado di manipolare i propri dati, e con questo, senza tanti giri di parole, avete già la definizione di oggetto: un insieme di dati incapsulati assieme a funzioni per manipolarli.

Anche una stringa in realtà è un oggetto dotato di molti metodi utili (in realtà in Python praticamente tutto è un oggetto), potete creare i vostri oggetti personali (analogamente alle funzioni), ma soprattutto potete usare le centinaia di oggetti già presenti nelle decine di moduli della libreria di Python, tra cui ad esempio quelli per accedere alla rete LAN e internet, per usare interfacce grafiche, accedere ai file su disco o creare programmi multitasking (che portano avanti più compiti nello stesso tempo).


CLASSI E ISTANZE

Le funzioni interne di un oggetto e dati di cui dispone (attributi), si descrivono con una classe. Una classe si usa come uno "stampo" per creare gli oggetti (istanze), che dispongono quindi di tutti i metodi e le variabili definiti nella classe stessa. La costruzione di un oggetto (creazione di un'istanza della classe) si ottiene "chiamando" la classe.

Dopo questa forse sconclusionata introduzione, ecco un esempio salvifico che chiarisce in maniera (spero) semplice quanto detto:

# Definisce una classe Persona e
# i due metodi  set_nome  e  saluta

class Persona:
 
    def set_nome(self, a):
        self.nome = a

    def saluta(self):
        print("Buon giorno, mi chiamo " + self.nome)

#-------------------------------------------------------------------------
# Crea due oggetti, istanze della classe Persona,
# assegnandoli alle variabili tizio1 tizio2
#-------------------------------------------------------------------------

tizio1 = Persona() # la chiamata a una classe crea un'istanza
tizio2 = Persona()

#-------------------------------------------------------------------------
# Usa i metodi degli oggetti
#-------------------------------------------------------------------------

tizio1.set_nome("Goffredo Mengiazzo")
tizio2.set_nome("Ton Cooper")
tizio1.saluta()
tizio2.saluta()

Ogni oggetto istanziato dalla classe 'Persona' ha la propria variabile interna personale (variabile di istanza) 'nome', e dispone dei metodi 'set_nome' e 'saluta'.

Il nome 'self' che compare come primo parametro nella definizione dei metodi può essere pensato come "l'oggetto stesso", infatti indica l'istanza a cui ci si riferisce in quel momento. Una classe definisce i metodi validi per tutte le istanze che verranno create a partire da essa, ma quando uno dei suoi metodi deve essere eseguito bisogna sapere a quale istanza riferirsi, e questa informazione è "trasmessa" con il parametro 'self' (implicitamente nell'uso dell'oggetto, esplicitamente come primo parametro nella definizione dei metodi e nell'uso delle variabili interne).

E' questo che vi permette di avere tante persone con nomi diversi, come tante liste o stringhe diverse che contengono ciascuna i propri dati... la possibilità di generare istanze multiple da una stessa classe è appunto una delle prime cose interessanti della OOP.

In particolare il nome 'self' all'interno della classe va sempre indicato ogni volta che ci si deve riferire all'istanza o qualcosa appartenente all'istanza (chiamate ai propri metodi comprese).


COSTRUTTORE E INIZIALIZZAZIONE

Una chiamata ad una classe crea (costruisce) un'istanza.
Un'operazione di "costruzione" l'avete già fatta nel momento in cui avete scritto tizio1 = Persona(). In più, se presente, al momento della costruzione si avvia automaticamente un metodo di nome __init__ (comunemente chiamato costruttore) grazie al quale è comodo fornire una serie di argomenti per inizializzare l'oggetto al momento stesso della sua creazione.

class Persona:

    def __init__(self, a):
        self.nome = a

    def saluta(self):
        print("Buon giorno, mi chiamo " + self.nome)

#-------------------------------------------------------------------------

tizio1 = Persona("Felice Pago") # crea istanza e inizializza
tizio2 = Persona("Goffredo Mengiazzo")
tizio1.saluta()
tizio2.saluta()

L' EREDITARIETÀ

Con il meccanismo dell' ereditarietà si possono creare nuovi oggetti a partire da quelli preesistenti aggiungendo comportamenti (metodi) nuovi. Nell'esempio seguente creiamo una classe 'PersonaGaia', derivata da 'Persona', che oltre a salutare sa anche cantare:

class PersonaGaia(Persona):

    def canta(self):
       print("Quel mazzolin di fiori...")

Come potete vedere, specificando tra parentesi un nome di classe nella definizione della nuova classe (classe derivata o sottoclasse), ereditate tutto da quella vecchia (sovraclasse), costruttore, metodi, attributi, e in più gli oggetti creati dalla nuova classe 'PersonaGaia' dispongono anche del nuovo metodo 'canta'. In pratica, tramite l'ereditarietà si creano delle sottoclassi più "specializzate".

tizio3 = PersonaGaia("Nello Rava")
tizio3.saluta()
tizio3.canta()

OVERRIDING

Se in una classe derivata definite un metodo che ha lo stesso nome di un metodo già presente nella sovraclasse, ne effettuate l'overriding (sovrascrittura, ridefinizione), e tutti gli oggetti istanziati dalla nuova classe useranno il nuovo metodo.

Possiamo ad esempio derivare da 'Persona' delle classi 'PersonaTedesca' e 'PersonaInglese' che salutano e cantano nella loro lingua (il metodo 'saluta' sovrascrive quello di 'Persona'):

class PersonaTedesca(Persona):

    def saluta(self):
        print("Guten Morgen, mein Name ist " + self.nome)

    def canta(self):
        print("O Tannenbaum o Tannenbaum...")

#-------------------------------------------------------------------------

class PersonaInglese(Persona):

    def saluta(self):
        print("Good morning, my name is " + self.nome)

    def canta(self):
        print("Happy birthday for you...")

#-------------------------------------------------------------------------

tizi = []     # Lista di persone
tizi.append(PersonaGaia("Nello Rava"))
tizi.append(PersonaTedesca("Otto Vaske"))
tizi.append(PersonaInglese("Ton Cooper"))
for tizio in tizi:
    tizio.saluta()
    tizio.canta()

CHIAMARE I METODI DELLA SOVRACLASSE

Se in una classe derivata avete ridefinito un metodo presente nella sovraclasse, ma per qualche motivo avete bisogno di chiamare anche il metodo "originale", basta chiamare esplicitamente il metodo della sovraclasse, un caso tipico è __init__:
class Discendente(Genitore):     # deriva una classe da Genitore
    def __init__(self):          # __init__ nuova classe
        Genitore.__init__(self)  # chiama __init__ sovraclasse
        ......                   # altre istruzioni del nuovo __init__

PERCHÉ USARE GLI OGGETTI?

All'interno di un programma gli oggetti (istanze) si possono pensare come cose/entità indipendenti, molto simili alle "cose" del mondo reale, hanno proprietà e caratteristiche individuali, e possono avere "comportamenti". È una grande astrazione che permette di pensare allo specifico oggetto come ad una cosa da usare, alla pari di un frullatore o di un telecomando, senza bisogno di sapere come questo frullatore o telecomando sono fatti al loro interno. Possono essere arbitrariamente complessi, ma tutto quello che ci serve sapere è solo il modo di usarli. Allo stesso modo, di un oggetto software ci basta sapere quali sono metodi per usarlo (interfaccia).

Ad esempio immaginiamo che in un programma ci serva l'equivalente di un contatore meccanico, resettabile, impostabile ad un certo valore, incrementabile o decrementabile, e magari anche con la funzionalità di rollover (superato il limite massimo ritorna a zero o decrementato sotto lo zero torna al limite massimo).

Strutturando molto male il programma potremmo usare una variabile per il conteggio, e scrivere una serie di piccole funzioni per realizzare tutti i compiti:

def incrementa():
    n = varcont + 1
    if n > limite:
        return 0
    else:
        return n

#-------------------------------------------------------------------------

def decrementa():
    n = varcont - 1
    if n < 0:
        return limite
    else:
        return n

#-------------------------------------------------------------------------

def resetta():
    return 0

#-------------------------------------------------------------------------

varcont = 10
limite = 12
varcont = incrementa()
varcont = incrementa()
varcont = incrementa()
varcont = incrementa()
print(varcont)                # --> 1
varcont = decrementa()
varcont = decrementa()
print(varcont)                # --> 12
varcont = resetta()
print(varcont)                # --> 0

Ma come fare se ad un certo momento abbiamo bisogno di cinque contatori che funzionano in modo indipendente l'uno dall'altro? Potremmo scrivere una grande quantità di funzioni simili che operano però su variabili diverse (pessimo!!), oppure potremmo creare due liste contenenti i valori dei contatori e dei relativi limiti, e passare alle funzioni l'indice del contatore voluto (già meglio, in effetti senza oggetti questa sarebbe probabilmente la strada da percorrere).

Descrivere un contatore con una apposita classe invece consente non solo di scrivere una volta sola le funzioni, ma elimina anche la necessità delle liste e dell'accesso tramite indice, in quanto ogni oggetto incapsula già i propri dati:

class Contatore:

    def __init__(self):
        self.varcont = 0
        self.limite = 0


    def imposta(self, n):
        self.varcont = n


    def limita(self, n):
        self.limite = n


    def resetta(self):
        self.varcont = 0


    def incrementa(self):
        n = self.varcont + 1
        if n > self.limite:
            self.varcont = 0
        else:
            self.varcont = n


    def decrementa(self):
        n = self.varcont - 1
        if n < 0:
            self.varcont = self.limite
        else:
            self.varcont = n


    def leggi(self):
        return self.varcont

#-------------------------------------------------------------------------

c = Contatore()
c.imposta(10)
c.limita(12)
c.incrementa()
c.incrementa()
c.incrementa()
c.incrementa()
print(c.leggi())
c.decrementa()
c.decrementa()
print(c.leggi())
c.resetta()
print(c.leggi())

'c' è un'istanza della classe 'Contatore', è un oggetto di uso più intuitivo rispetto alle funzioni dell'esempio precedente, non ci deve preoccupare né di indici né di nomi di variabili, è a tutti gli effetti "una cosa" da usare con i suoi cinque "comandi": imposta limita incrementa decrementa leggi. Ma soprattutto la classe permette di creare quanti contatori indipendenti si vogliono senza alcuna modifica:

c2 = Contatore()
c3 = Contatore()
c4 = Contatore()
....

Nel prossimo esempio un'anteprima di ciò che si può ottenere con le classi di una libreria grafica (in questo caso tkinter già inclusa nella distribuzione di Python). 'finestra' e 'superficie' sono due oggetti istanze delle classi Tk e Canvas (contenute nel modulo tkinter), questi oggetti sono visibili, cioè si presentano visivamente sullo schermo (in particolare 'superficie' è un'area grafica utilizzabile per disegnare semplici figure interna a 'finestra').

La classe 'Palla' descrive invece il comportamento di una singola palla rimbalzante, si muove di un passo chiamando il suo metodo 'muovi', e contiene anche un riferimento alla superficie su cui disegnarsi.

Nel blocco principale del programma viene creata una lista di palle, che vengono fatte movere ogni 30 millisecondi con la funzione 'muovi' (da non confondere con il metodo muovi degli oggetti 'Palla'). Questa funzione si riavvia automaticamente grazie al metodo 'after' dell'oggetto 'finestra'.

Siccome ogni palla è un singolo oggetto indipendente, come nel caso dei contatori descritti prima è possibile senza alcun problema creare una lista con un numero qualsiasi di palle (in questo caso 5).

import tkinter
import random

#-------------------------------------------------------------------------

class Palla:

    def __init__(self, superficie):
        self.x = random.randint(20, 300)
        self.y = random.randint(20, 180)
        self.vx = random.randint(5, 12)
        self.vy = random.randint(3, 10)
        self.superficie = superficie
        self.figura = superficie.create_oval(0, 0, 0, 0, fill="green")
        
    def muovi(self):
        self.x = self.x + self.vx
        if self.x <= 15 or self.x > 305:  self.vx = -self.vx
        self.y = self.y + self.vy
        if self.y <= 15 or self.y > 185:  self.vy = -self.vy
        self.superficie.coords(self.figura, self.x-15, 
                               self.y-15, self.x+15, self.y+15)

#-------------------------------------------------------------------------

def muovi(finestra, lista_palle):
    finestra.after(30, muovi, finestra, lista_palle)
    for palla in lista_palle:  palla.muovi()
    
#-------------------------------------------------------------------------

def main():
    finestra = tkinter.Tk()
    superficie = tkinter.Canvas(finestra, width=320, height=200)
    superficie.pack()
    superficie.configure(bg="black")
    lista_palle = []
    for n in range(5):  lista_palle.append(Palla(superficie))
    muovi(finestra, lista_palle)
    finestra.mainloop()
    
#-------------------------------------------------------------------------

main()

Nota: in Python2 sostituire tkinter con Tkinter


 ☗   ◀   ☰   ▶