Previous Up Next

Buy this book at Amazon.com

Chapter 15  Classi e oggetti

A 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 personalizzati

Abbiamo 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:

  • Memorizzare le coordinate in due variabili separate, x e y.
  • Memorizzare le coordinate come elementi di una lista o di una tupla.
  • Creare un nuovo tipo che rappresenti i punti come degli oggetti.

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” è __main__.Punto.

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  Attributi

Potete 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 .


Figure 15.1: 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 riceve come argomento un Punto e lo visualizza in notazione matematica. Per invocarla, passate nuovo come argomento:

>>> 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 distanza_tra_punti che riceva due Punti come argomenti e ne restituisca la distanza.

15.3  Rettangoli

A 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:

  • Definire il centro del rettangolo oppure un angolo, e le sue dimensioni (altezza e larghezza);
  • Definire due angoli opposti.

È 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.”


Figure 15.2: Diagramma di oggetto.

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 ritorno

Le funzioni possono restituire istanze. Per esempio, trova_centro prende un oggetto Rettangolo come argomento e restituisce un oggetto Punto che contiene le coordinate del centro di Rettangolo:

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 mutabili

Potete 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, accresci_rettangolo prende un oggetto Rettangolo e due numeri, dlargh e dalt, e li aggiunge alla larghezza e all’altezza del rettangolo:

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 sposta_rettangolo che prenda come parametri un Rettangolo e due valori dx e dy. La funzione deve spostare il rettangolo nel piano, aggiungendo dx alla coordinata x di angolo, e aggiungendo dy alla coordinata y di angolo.

15.6  Copia

Abbiamo 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

Figure 15.3: Diagramma di oggetto.

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 accresci_rettangolo su uno dei Rettangoli non influenzerebbe l’altro, ma invocare sposta_rettangolo su uno dei due, influenzerebbe entrambi! Tutto ciò genera confusione ed è foriero di errori.

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 sposta_rettangolo che crei e restituisca un nuovo Rettangolo anziché modificare quello di origine.

15.7  Debug

Iniziando 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

classe:
Tipo di dato personalizzato definito dal programmatore. Una definizione di classe crea un nuovo oggetto classe.
oggetto classe:
Oggetto che contiene le informazioni su un tipo personalizzato e che può essere usato per creare istanze del tipo.
istanza:
Oggetto che appartiene ad una classe.
instanziare:
Creare un nuovo oggetto.
attributo:
Uno dei valori associati ad un oggetto, dotato di un nome.
oggetto contenuto (embedded):
Oggetto che è contenuto come attributo di un altro oggetto (detto contenitore).
copia shallow:
copia “superficiale” dei contenuti di un oggetto, senza includere alcun riferimento ad eventuali oggetti contenuti; è implementata grazie alla funzione copy del modulo copy.
copia profonda:
Copia del contenuto di un oggetto e anche degli eventuali oggetti interni e degli oggetti a loro volta contenuti in essi; è implementata grazie alla funzione deepcopy del modulo copy.
diagramma di oggetto:
Diagramma che mostra gli oggetti, i loro attributi e i valori di questi ultimi.

15.9  Esercizi

Esercizio 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 punto_nel_cerchio, che prenda un Cerchio e un Punto e restituisca True se il punto giace dentro il cerchio, circonferenza compresa.

Scrivete una funzione di nome rett_nel_cerchio, che prenda un Cerchio e un Rettangolo e restituisca True se il rettangolo giace interamente all’interno del cerchio, circonferenza compresa.

Scrivete una funzione di nome rett_cerchio_sovrapp, che prenda un Cerchio e un Rettangolo e restituisca True se almeno uno degli angoli del Rettangolo ricade all’interno del cerchio. Oppure, più difficile, se una qualunque porzione del Rettangolo ricade all’interno del cerchio.

Soluzione: http://thinkpython2.com/code/Circle.py.


Esercizio 2  

Scrivete una funzione di nome disegna_rett che prenda un oggetto Turtle e un Rettangolo e usi la Tartaruga per disegnare il Rettangolo. Vedere il Capitolo 4 per esempi di uso degli oggetti Turtle.

Scrivete una funzione di nome disegna_cerchio che prenda un oggetto Turtle e un Cerchio, e disegni il Cerchio.

Soluzione: http://thinkpython2.com/code/draw.py.

Buy this book at Amazon.com

Contribute

If you would like to make a contribution to support my books, you can use the button below. Thank you!
Pay what you want:

Are you using one of our books in a class?

We'd like to know about it. Please consider filling out this short survey.



Previous Up Next