Chapter 15 Classi e oggettiA questo punto, sapete come usare le funzioni per organizzare il codice, e i tipi predefiniti per organizzare i dati. Il passo successivo è imparare la programmazione orientata agli oggetti, che usa tipi personalizzati in modo da organizzare sia il codice che i dati. La programmazione orientata agli oggetti è un argomento vasto, per addentrarsi nel quale occorrono alcuni capitoli. Il codice degli esempi di questo capitolo è scaricabile dal sito http://thinkpython2.com/code/Point1.py; le soluzioni degli esercizi da http://thinkpython2.com/code/Point1_soln.py. 15.1 Tipi personalizzatiAbbiamo usato molti dei tipi predefiniti in Python, e ora siamo pronti per crearne uno nuovo: come esempio, creeremo un tipo che chiameremo Punto, che rappresenta un punto in un piano cartesiano bidimensionale. Nella notazione matematica, il punto è denotato da una coppia ordinata di numeri, dette coordinate; le coordinate dei punti sono spesso scritte tra parentesi con una virgola che separa i due valori. Per esempio, (0,0) rappresenta l’origine e (x,y) il punto che si trova a x unità a destra e y unità in alto rispetto all’origine. Ci sono alcuni modi per rappresentare i punti in Python:
L’ultima opzione è più complessa delle altre, ma ha dei vantaggi che saranno presto chiariti. Un tipo personalizzato, definito dal programmatore, è chiamato anche classe. Una definizione di classe ha questa sintassi: class Punto: """Rappresenta un punto in un piano.""" L’intestazione indica che la nuova classe si chiama Punto. Il corpo è una stringa di documentazione che spiega cosa fa la classe. Al suo interno si possono poi definire metodi e variabili, ma ci arriveremo tra poco. La definizione di una classe di nome Punto crea un oggetto classe. >>> Punto <class '__main__.Punto'> Poiché la classe Punto è stata definita al livello principale, il suo “cognome e nome” è L’oggetto classe è simile ad uno stampo che ci permette di fabbricare degli oggetti. Per creare un nuovo oggetto Punto, basta chiamare Punto come se fosse una funzione. >>> nuovo = Punto() >>> nuovo <__main__.Punto object at 0xb7e9d3ac> Il valore di ritorno è un riferimento ad un oggetto Punto, che qui abbiamo assegnato alla variabile nuovo. La creazione di un nuovo oggetto è detta istanziazione, e l’oggetto è un’istanza della classe. Quando stampate un’istanza, Python informa a quale classe appartiene e in quale posizione di memoria è collocata (il prefisso 0x significa che il numero che segue è in formato esadecimale). Ogni oggetto è un’istanza di una qualche classe, per cui i termini “oggetto” ed “istanza” sono equivalenti. In questa sede, utilizzerò “istanza” per indicare che sto parlando di un tipo personalizzato. 15.2 AttributiPotete assegnare dei valori ad un’istanza usando la notazione a punto: >>> nuovo.x = 3.0 >>> nuovo.y = 4.0 Questa sintassi è simile a quella usata per la selezione di una variabile appartenente ad un modulo, tipo math.pi o string.whitespace. In questo caso però, stiamo assegnando dei valori a degli elementi di un oggetto, ai quali è stato attribuito un nome (x e y). Questi elementi sono detti attributi. Il diagramma di stato in Figura 15.1 mostra il risultato delle assegnazioni. Un diagramma di stato che illustra un oggetto e i suoi attributi è detto diagramma di oggetto . La variabile nuovo fa riferimento ad un oggetto Punto che contiene due attributi, ed ogni attributo fa riferimento ad un numero in virgola mobile. Potete leggere il valore di un attributo usando la stessa sintassi: >>> nuovo.y 4.0 >>> x = nuovo.x >>> x 3.0 L’espressione nuovo.x significa: “Vai all’oggetto a cui nuovo fa riferimento e prendi il valore di x”. In questo esempio, assegniamo il valore ad una variabile di nome x. Non c’è conflitto tra la variabile locale x e l’attributo x. Potete usare la notazione a punto all’interno di qualunque espressione, per esempio: >>> print('(%g, %g)' % (nuovo.x, nuovo.y)) (3.0, 4.0) >>> distanza = math.sqrt(nuovo.x**2 + nuovo.y**2) >>> distanza 5.0 Potete anche passare un’istanza come argomento, nel modo consueto: def stampa_punto(p): print('(%g, %g)' % (p.x, p.y)) La funzione >>> stampa_punto(nuovo) (3.0, 4.0) Dentro alla funzione, il parametro p è un alias di nuovo, quindi se la funzione modifica p, anche nuovo viene modificato di conseguenza. Per esercizio, scrivete una funzione di nome 15.3 RettangoliA volte è abbastanza ovvio stabilire gli attributi necessari ad un oggetto, ma in altre occasioni occorre fare delle scelte. Immaginate di progettare una classe che rappresenti un rettangolo: quali attributi dovete usare per specificarne le dimensioni e la collocazione nel piano? Per semplicità, ignorate l’inclinazione e supponete che il rettangolo sia allineato in orizzontale o verticale. Ci sono almeno due possibili scelte:
È difficile stabilire quale delle due opzioni sia la migliore, ma giusto per fare un esempio implementeremo la prima. Definiamo la nuova classe: class Rettangolo: """Rappresenta un rettangolo. attributi: larghezza, altezza, angolo. """ La docstring elenca gli attributi: larghezza e altezza sono numeri; angolo è un oggetto Punto che identifica l’angolo in basso a sinistra. Per ottenere una rappresentazione di un rettangolo, dovete istanziare un oggetto Rettangolo e assegnare dei valori ai suoi attributi: box = Rettangolo() box.larghezza = 100.0 box.altezza = 200.0 box.angolo = Punto() box.angolo.x = 0.0 box.angolo.y = 0.0 L’espressione box.angolo.x significa: “Vai all’oggetto a cui box fa riferimento e seleziona l’attributo chiamato angolo; poi vai a quell’oggetto e seleziona l’attributo chiamato x.” La Figura 15.2 mostra lo stato di questo oggetto. Un oggetto che è un attributo di un altro oggetto è detto oggetto contenuto (embedded). 15.4 Istanze come valori di ritornoLe funzioni possono restituire istanze. Per esempio, def trova_centro(rett): p = Punto() p.x = rett.angolo.x + rett.larghezza/2 p.y = rett.angolo.y + rett.altezza/2 return p Ecco un esempio che passa box come argomento e assegna il Punto risultante a centro: >>> centro = trova_centro(box) >>> stampa_punto(centro) (50, 100) 15.5 Gli oggetti sono mutabiliPotete cambiare lo stato di un oggetto con un’assegnazione ad uno dei suoi attributi. Per esempio, per cambiare le dimensioni di un rettangolo senza cambiarne la posizione, potete modificare i valori di larghezza e altezza: box.larghezza = box.larghezza + 50 box.altezza = box.altezza + 100 Potete anche scrivere delle funzioni che modificano oggetti. Per esempio,
def accresci_rettangolo(rett, dlargh, dalt): rett.larghezza += dlargh rett.altezza += dalt Ecco un esempio dell’effetto della funzione: >>> box.larghezza, box.altezza (150.0, 300.0) >>> accresci_rettangolo(box, 50, 100) >>> box.larghezza, box.altezza (200.0, 400.0) Dentro la funzione, rett è un alias di box, pertanto quando la funzione modifica rett, anche box cambia. Come esercizio, scrivete una funzione di nome 15.6 CopiaAbbiamo già visto che gli alias possono rendere il programma difficile da leggere, perché una modifica in un punto del programma può dare degli effetti inattesi in un altro punto. Non è semplice tenere traccia di tutte le variabili che potrebbero fare riferimento ad un dato oggetto. La copia di un oggetto è spesso una comoda alternativa all’alias. Il modulo copy contiene una funzione, anch’essa di nome copy, che permette di duplicare qualsiasi oggetto: >>> p1 = Punto() >>> p1.x = 3.0 >>> p1.y = 4.0 >>> import copy >>> p2 = copy.copy(p1) p1 e p2 contengono gli stessi dati, ma non sono lo stesso Punto. >>> stampa_punto(p1) (3, 4) >>> stampa_punto(p2) (3, 4) >>> p1 is p2 False >>> p1 == p2 False L’operatore is indica che p1 e p2 non sono lo stesso oggetto, come volevasi dimostrare. Ma forse vi aspettavate che l’operatore == desse True, perché i due punti contengono gli stessi dati. Invece, dovete sapere che, nel caso di istanze, il comportamento predefinito dell’operatore == è lo stesso dell’operatore is: controlla l’identità dell’oggetto e non l’equivalenza. Questo perché, per i tipi personalizzati, Python non sa cosa debba essere considerato equivalente. O almeno, non lo sa ancora. Nell’usare copy.copy per duplicare un Rettangolo, noterete che copia l’oggetto Rettangolo ma non l’oggetto Punto contenuto. >>> box2 = copy.copy(box) >>> box2 is box False >>> box2.angolo is box.angolo True La Figura 15.3 mostra la situazione del diagramma di oggetto. Questa operazione è chiamata copia shallow (o copia superficiale) perché copia l’oggetto ed ogni riferimento che contiene, ma non gli oggetti contenuti. Nella maggior parte dei casi, questo non è il comportamento ideale. Nel nostro esempio, invocare Fortunatamente, il modulo copy è dotato anche di un altro metodo chiamato deepcopy che non solo copia l’oggetto, ma anche gli oggetti a cui si riferisce, e gli oggetti a cui questi ultimi a loro volta si riferiscono, e così via. Non vi sorprenderà che questa si chiami copia profonda. >>> box3 = copy.deepcopy(box) >>> box3 is box False >>> box3.angolo is box.angolo False box3 e box sono oggetti completamente diversi. Come esercizio, scrivete una versione di 15.7 DebugIniziando a lavorare con gli oggetti, è facile imbattersi in alcuni nuovi tipi di eccezioni. Se cercate di accedere ad un attributo che non esiste, si verifica un AttributeError: >>> p = Punto() >>> p.x = 3 >>> p.y = 4 >>> p.z AttributeError: Punto instance has no attribute 'z' Se non siete sicuri di che tipo sia un oggetto, potete chiederlo: >>> type(p) <class '__main__.Punto'> Si può usare anche isinstance per controllare se un oggetto è un’istanza di una classe: >>> isinstance(p, Punto) True Se volete sapere se un oggetto ha un certo attributo, usate la funzione predefinita hasattr: >>> hasattr(p, 'x') True >>> hasattr(p, 'z') False Il primo argomento può essere un qualunque oggetto, il secondo è una stringa che contiene il nome dell’attributo. Si può anche usare un’istruzione try per controllare che l’oggetto contenga gli attributi che servono: try: x = p.x except AttributeError: x = 0 Questa tecnica può facilitare la scrittura di funzioni che trattano tipi di dati differenti; vedremo altro su questo tema nel Paragrafo 17.9. 15.8 Glossario
15.9 EserciziEsercizio 1 Scrivete una definizione di classe di nome Cerchio, avente gli attributi centro e raggio, dove centro è un oggetto Punto e raggio è un numero. Istanziate un oggetto Cerchio che rappresenti un cerchio con il centro nel punto (150, 100) e di raggio 75. Scrivete una funzione di nome Scrivete una funzione di nome Scrivete una funzione di nome Soluzione: http://thinkpython2.com/code/Circle.py.
Esercizio 2
Scrivete una funzione di nome Scrivete una funzione di nome Soluzione: http://thinkpython2.com/code/draw.py. |
ContributeIf you would like to make a contribution to support my books, you can use the button below. Thank you!
Are you using one of our books in a class?We'd like to know about it. Please consider filling out this short survey.
|