Next Up Previous Hi Index

Chapter 12

Classi e oggetti

12.1 Tipi composti definiti dall'utente

Abbiamo usato alcuni dei tipi composti predefiniti e ora siamo pronti per crearne uno tutto nostro: il tipo Punto.

Considerando il concetto matematico di punto nelle due dimensioni, il punto è definito da una coppia di numeri (le coordinate). In notazione matematica le coordinate dei punti sono spesso scritte tra parentesi con una virgola posta a separare i due valori. Per esempio (0, 0) rappresenta l'origine e (x, y) il punto che si trova x unità a destra e y unità in alto rispetto all'origine.

Un modo naturale di rappresentare un punto in Python è una coppia di numeri in virgola mobile e la questione che ci rimane da definire è in che modo raggruppare questa coppia di valori in un oggetto composto: un sistema veloce anche se poco elegante sarebbe l'uso di una tupla, anche se possiamo fare di meglio.

Un modo alternativo è quello di definire un nuovo tipo composto chiamato classe. Questo tipo di approccio richiede un po' di sforzo iniziale, ma i suoi benefici saranno subito evidenti.

Una definizione di classe ha questa sintassi:

class Punto:
  pass

Le definizioni di classe possono essere poste in qualsiasi punto di un programma ma solitamente per questioni di leggibilità sono poste all'inizio, subito sotto le istruzioni import. Le regole di sintassi per la definizione di una classe sono le stesse degli altri tipi composti: la definizione dell'esempio crea una nuova classe chiamata Punto. L'istruzione pass non ha effetti: è stata usata per il solo fatto che la definizione prevede un corpo che deve ancora essere scritto.

Creando la classe Punto abbiamo anche creato un nuovo tipo di dato chiamato con lo stesso nome. I membri di questo tipo sono detti istanze del tipo o oggetti. La creazione di una nuova istanza è detta istanziazione: solo al momento dell'istanziazione parte della memoria è riservata per depositare il valore dell'oggetto. Per creare un oggetto di tipo Punto viene chiamata una funzione chiamata Punto:

P1 = Punto()

Alla variabile P1 è assegnato il riferimento ad un nuovo oggetto Punto. Una funzione come Punto, che crea nuovi oggetti e riserva quindi della memoria per depositarne i valori, è detta costruttore.

12.2 Attributi

Possiamo aggiungere un nuovo dato ad un'istanza usando la notazione punto:

>>> P1.x = 3.0
>>> P1.y = 4.0

Questa sintassi è simile a quella usata per la selezione di una variabile appartenente ad un modulo, tipo math.pi e string.uppercase. In questo caso stiamo selezionando una voce da un'istanza e queste voci che fanno parte dell'istanza sono dette attributi.

Questo diagramma di stato mostra il risultato delle assegnazioni:

La variabile P1 si riferisce ad un oggetto Punto che contiene due attributi ed ogni attributo (una coordinata) si riferisce ad un numero in virgola mobile.

Possiamo leggere il valore di un attributo con la stessa sintassi:

>>> print P1.y
4.0
>>> x = P1.x
>>> print x
3.0

L'espressione P1.x significa "vai all'oggetto puntato da P1 e ottieni il valore del suo attributo x". In questo caso assegniamo il valore ad una variabile chiamata x: non c'è conflitto tra la variabile locale x e l'attributo x di P1: lo scopo della notazione punto è proprio quello di identificare la variabile cui ci si riferisce evitando le ambiguità.

Puoi usare la notazione punto all'interno di ogni espressione così che le istruzioni proposte di seguito sono a tutti gli effetti perfettamente lecite:

print '(' + str(P1.x) + ', ' + str(P1.y) + ')'
DistanzaAlQuadrato = P1.x * P1.x + P1.y * P1.y

La prima riga stampa (3.0, 4.0); la seconda calcola il valore 25.0.

Potresti essere tentato di stampare direttamente il valore di P1:

>>> print P1
<__main__.Punto instance at 80f8e70>

Il risultato indica che P1 è un'istanza della classe Punto e che è stato definito in __main__. 80f8e70 è l'identificatore univoco dell'oggetto, scritto in base 16 (esadecimale). Probabilmente questo non è il modo più pratico di mostrare un oggetto Punto ma vedrai subito come renderlo più comprensibile.

Esercizio: crea e stampa un oggetto Punto e poi usa id per stampare l'identificatore univoco dell'oggetto. Traduci la forma esadecimale dell'identificatore in decimale e verifica che i due valori trovati coincidono.

12.3 Istanze come parametri

Puoi passare un'istanza come parametro ad una funzione nel solito modo:

def StampaPunto(Punto):
  print '(' + str(Punto.x) + ', ' + str(Punto.y) + ')'

StampaPunto prende un oggetto Punto come argomento e ne stampa gli attributi in forma standard. Se chiami StampaPunto(P1) la stampa è (3.0, 4.0).

Esercizio: riscrivi la funzione DistanzaTraDuePunti che abbiamo già visto alla sezione 5.2 così da accettare due oggetti di tipo Punto invece di quattro numeri.

12.4 Uguaglianza

La parola "uguale" sembra così intuitiva che probabilmente non hai mai pensato più di tanto a cosa significa veramente.

Quando dici "Alberto ed io abbiamo la stessa auto" naturalmente vuoi dire che entrambi possedete un'auto dello stesso modello ed è sottinteso che stai parlando di due auto diverse e non di una soltanto. Se dici "Alberto ed io abbiamo la stessa madre" è sottinteso che la madre è la stessa e voi siete fratelli * Note. L'idea stessa di uguaglianza dipende quindi dal contesto.

Quando parli di oggetti abbiamo la stessa ambiguità: se due oggetti di tipo Punto sono gli stessi, significa che hanno semplicemente gli stessi dati (coordinate) o che si sta parlando di un medesimo oggetto?

Per vedere se due riferimenti fanno capo allo stesso oggetto usa l'operatore ==:

>>> P1 = Punto()
>>> P1.x = 3
>>> P1.y = 4
>>> P2 = Punto()
>>> P2.x = 3
>>> P2.y = 4
>>> P1 == P2
0

Anche se P1 e P2 hanno le stesse coordinate non fanno riferimento allo stesso oggetto ma a due oggetti diversi. Se assegniamo P1 a P2 allora le due variabili sono alias dello stesso oggetto:

>>> P2 = P1
>>> P1 == P2
1

Questo tipo di uguaglianza è detta uguaglianza debole perché si limita a confrontare solo i riferimenti delle variabili e non il contenuto degli oggetti.

Per confrontare il contenuto degli oggetti (uguaglianza forte) possiamo scrivere una funzione chiamata StessoPunto:

def StessoPunto(P1, P2) :
  return (P1.x == P2.x) and (P1.y == P2.y)

Se creiamo due differenti oggetti che contengono gli stessi dati possiamo ora usare StessoPunto per verificare se entrambi rappresentano lo stesso punto:

>>> P1 = Punto()
>>> P1.x = 3
>>> P1.y = 4
>>> P2 = Punto()
>>> P2.x = 3
>>> P2.y = 4
>>> StessoPunto(P1, P2)
1

Logicamente se le due variabili si riferiscono allo stesso punto e sono alias l'una dell'altra allo stesso tempo garantiscono l'uguaglianza debole e quella forte.

12.5 Rettangoli

Se volessimo creare una classe per rappresentare un rettangolo quali informazioni dovremmo fornire per specificarlo in modo univoco? Per rendere le cose più semplici partiremo con un rettangolo orientato lungo gli assi.

Ci sono poche possibilità tra cui scegliere: potremmo specificare il centro del rettangolo e le sue dimensioni (altezza e larghezza); oppure specificare un angolo di riferimento e le dimensioni (ancora altezza e larghezza); o ancora specificare le coordinate di due punti opposti. Una scelta convenzionale abbastanza comune è quella di specificare il punto in alto a sinistra e le dimensioni.

Definiamo la nuova classe:

class Rettangolo:
  pass

Per istanziare un nuovo oggetto rettangolo:

Rett = Rettangolo()
Rett.Larghezza = 100.0
Rett.Altezza = 200.0

Questo codice crea un nuovo oggetto Rettangolo con due attributi in virgola mobile. Ci manca solo il punto di riferimento in alto a sinistra e per specificarlo possiamo inserire un oggetto all'interno di un altro oggetto:

Rett.AltoSinistra = Punto()
Rett.AltoSinistra.x = 0.0;
Rett.AltoSinistra.y = 0.0;

L'operatore punto è usato per comporre l'espressione: Rett.AltoSinistra.x significa "vai all'oggetto cui si riferisce Rett e seleziona l'attributo chiamato AltoSinistra; poi vai all'oggetto cui si riferisce AltoSinistra e seleziona l'attributo chiamato x."

La figura mostra lo stato di questo oggetto:

12.6 Istanze come valori di ritorno

Le funzioni possono ritornare istanze. Possiamo quindi scrivere una funzione TrovaCentro che prende un oggetto Rettangolo come argomento e restituisce un oggetto Punto che contiene le coordinate del centro del rettangolo:

def TrovaCentro(Rettangolo):
  P = Punto()
  P.x = Rettangolo.AltoSinistra.x + Rettangolo.Larghezza/2.0
  P.y = Rettangolo.AltoSinistra.y + Rettangolo.Altezza/2.0
  return P

Per chiamare questa funzione passa Rett come argomento e assegna il risultato ad una variabile:

>>> Centro = TrovaCentro(Rett)
>>> StampaPunto(Centro)
(50.0, 100.0)

12.7 Gli oggetti sono mutabili

Possiamo cambiare lo stato di un oggetto facendo un'assegnazione ad uno dei suoi attributi. Per fare un esempio possiamo cambiare le dimensioni di Rett:

Rett.Larghezza = Rett.Larghezza + 50
Rett.Altezza = Rett.Altezza + 100

Incapsulando questo codice in un metodo e generalizzandolo diamo la possibilità di aumentare le dimensioni di qualsiasi rettangolo:

def AumentaRettangolo(Rettangolo, AumentoLargh, AumentoAlt) :
  Rettangolo.Larghezza = Rettangolo.Larghezza + AumentoLargh;
  Rettangolo.Altezza = Rettangolo.Altezza + AumentoAlt;

Le variabili AumentoLargh e AumentoAlt indicano di quanto devono essere aumentate le dimensioni del rettangolo. Invocare questo metodo ha lo stesso effetto di modificare il Rettangolo che è passato come argomento.

Creiamo un nuovo rettangolo chiamato R1 e passiamolo a AumentaRettangolo:

>>> R1 = Rettangolo()
>>> R1.Larghezza = 100.0
>>> R1.Altezza = 200.0
>>> R1.AltoSinistra = Punto()
>>> R1.AltoSinistra.x = 0.0;
>>> R1.AltoSinistra.y = 0.0;
>>> AumentaRettangolo(R1, 50, 100)

Mentre stiamo eseguendo AumentaRettangolo il parametro Rettangolo è un alias per R1. Ogni cambiamento apportato a Rettangolo modifica direttamente R1 e viceversa.

Esercizio: scrivi una funzione chiamata MuoviRettangolo che prende come parametri un Rettangolo e due valori dx e dy. La funzione deve spostare le coordinate del punto in alto a sinistra sommando alla posizione x il valore dx e alla posizione y il valore dy.

12.8 Copia

Abbiamo già visto che gli alias possono rendere il programma difficile da leggere perché una modifica può cambiare il valore di variabili che apparentemente non hanno nulla a che vedere con quelle modificate. Man mano che le dimensioni del programma crescono diventa difficile tenere a mente quali variabili si riferiscano ad un dato oggetto.

La copia di un oggetto è spesso una comoda alternativa all'alias. Il modulo copy contiene una funzione copy che permette di duplicare qualsiasi oggetto:

>>> import copy
>>> P1 = Punto()
>>> P1.x = 3
>>> P1.y = 4
>>> P2 = copy.copy(P1)
>>> P1 == P2
0
>>> StessoPunto(P1, P2)
1

Dopo avere importato il modulo copy possiamo usare il metodo copy in esso contenuto per creare un nuovo oggetto Punto. P1 e P2 non solo sono lo stesso punto ma contengono gli stessi dati.

Per copiare un semplice oggetto come Punto che non contiene altri oggetti al proprio interno copy è sufficiente. Questa è chiamata copia debole:

>>> Punto2 = copy.copy(Punto1)

Quando abbiamo a che fare con un Rettangolo che contiene al proprio interno un riferimento ad un altro oggetto Punto, copy non lavora come ci si aspetta dato che viene copiato il riferimento a Punto così che sia il vecchio che il nuovo Rettangolo si riferiscono allo stesso oggetto invece di averne uno proprio per ciascuno.

Se creiamo il rettangolo R1 nel solito modo e ne facciamo una copia R2 usando copy il diagramma di stato risultante sarà:

Quasi certamente non è questo ciò che vogliamo. In questo caso, invocando AumentaRettangolo su uno dei rettangoli non si cambieranno le dimensioni dell'altro, ma MuoviRettangolo sposterà entrambi! Questo comportamento genera parecchia confusione e porta facilmente a commettere errori.

Fortunatamente il modulo copy contiene un altro metodo chiamato deepcopy che copia correttamente non solo l'oggetto ma anche gli eventuali oggetti presenti al suo interno:

>>> Oggetto2 = copy.deepcopy(Oggetto1)

Ora Oggetto1 e Oggetto2 sono oggetti completamente separati e occupano diverse zone di memoria.

Possiamo usare deepcopy per riscrivere completamente AumentaRettangolo così da non cambiare il Rettangolo originale ma restituire una copia con le nuove dimensioni:

def AumentaRettangolo(Rettangolo, AumentoLargh, AumentoAlt) :
  import copy
  NuovoRett = copy.deepcopy(Rettangolo)
  NuovoRett.Larghezza = NuovoRett.Larghezza + AumentoLargh
  NuovoRett.Altezza = NuovoRett.Altezza + AumentoAlt;
  return NuovoRett

Esercizio: riscrivi MuoviRettangolo per creare e restituire un nuovo rettangolo invece di modificare quello originale.

12.9 Glossario

Classe
tipo di dato composto definito dall'utente.
Istanziare
creare un'istanza di una determinata classe.
Istanza
oggetto che appartiene ad una classe.
Oggetto
tipo di dato composto che è spesso usato per definire un concetto o una cosa del mondo reale.
Costruttore
metodo usato per definire nuovi oggetti.
Attributo
uno dei componenti che costituiscono un'istanza.
Uguaglianza debole
uguaglianza di riferimenti che si verifica quando due variabili si riferiscono allo stesso oggetto.
Uguaglianza forte
uguaglianza di valori che si verifica quando due variabili si riferiscono a oggetti che hanno lo stesso valore.
Copia debole
copia del contenuto di un oggetto includendo ogni riferimento ad eventuali oggetti interni, realizzata con la funzione copy del modulo copy.
Copia forte
copia sia del contenuto di un oggetto che degli eventuali oggetti interni e degli oggetti eventualmente contenuti in essi; è realizzata dalla funzione deepcopy del modulo copy.


Next Up Previous Hi Index