Previous Up Next

Buy this book at Amazon.com

Chapter 17  Classi e metodi

Anche se abbiamo usato alcune delle caratteristiche object-oriented di Python, i programmi degli ultimi due capitoli non sono del tutto orientati agli oggetti, perché non mettono in evidenza le relazioni che esistono tra i tipi personalizzati e le funzioni che operano su di essi. Il passo successivo è di trasformare queste funzioni in metodi, in modo da rendere esplicite queste relazioni.

Il codice degli esempi di questo capitolo è scaricabile dal sito http://thinkpython2.com/code/Time2.py, e le soluzioni degli esercizi da http://thinkpython2.com/code/Point2_soln.py.

17.1  Funzionalità orientate agli oggetti

Python è un linguaggio di programmazione orientato agli oggetti, in altre parole contiene delle funzionalità a supporto della programmazione orientata agli oggetti, che ha le seguenti caratteristiche distintive:

  • I programmi includono definizioni di classi e metodi.
  • Buona parte dell’elaborazione è espressa in termini di operazioni sugli oggetti.
  • Gli oggetti corrispondono spesso ad un oggetto o concetto del mondo reale, mentre i metodi che operano sugli oggetti corrispondono spesso al modo in cui gli oggetti interagiscono tra loro nella realtà quotidiana.

Per esempio, la classe Tempo definita nel Capitolo 16 corrisponde al modo in cui le persone pensano alle ore del giorno, e le funzioni che abbiamo definite corrispondono al tipo di operazioni che le persone fanno con il tempo. Allo stesso modo, le classi Punto e Rettangolo nel Capitolo 15 corrispondono ai rispettivi concetti matematici.

Finora, non abbiamo tratto vantaggio dalle capacità di supporto della programmazione orientata agli oggetti fornite da Python. A dire il vero, queste funzionalità non sono indispensabili; piuttosto, forniscono una sintassi alternativa per fare le cose che abbiamo già fatto. Ma in molti casi questa alternativa è più concisa e si adatta in modo più accurato alla struttura del programma.

Ad esempio, nel programma Time1.py non c’è una chiara connessione tra la definizione della classe e le definizioni di funzione che seguono. A un esame più attento, è però evidente che tutte queste funzioni ricevono almeno un oggetto Tempo come argomento.

Questa osservazione giustifica l’esistenza dei metodi; un metodo è una funzione associata ad una particolare classe. Abbiamo già visto qualche metodo per le stringhe, le liste, i dizionari e le tuple. In questo capitolo, definiremo dei metodi per i tipi personalizzati.

Da un punto di vista logico, i metodi sono la stessa cosa delle funzioni, ma con due differenze sintattiche:

  • I metodi sono definiti all’interno di una definizione di classe, per rendere esplicita la relazione tra la classe stessa ed il metodo.
  • La sintassi per invocare un metodo è diversa da quella usata per chiamare una funzione.

Nei prossimi paragrafi prenderemo le funzioni scritte nei due capitoli precedenti e le trasformeremo in metodi. Questa trasformazione è puramente meccanica e si fa seguendo una serie di passi: se siete in grado di convertire da funzione a metodo e viceversa, riuscirete anche a scegliere la forma migliore, qualsiasi cosa dobbiate fare.

17.2  Stampa di oggetti

Nel Capitolo 16, abbiamo definito una classe chiamata Tempo, e nel Paragrafo 16.1, avete scritto una funzione di nome stampa_tempo:

class Tempo:
    """Rappresenta un'ora del giorno."""

def stampa_tempo(tempo):
    print('%.2d:%.2d:%.2d' % (tempo.ora, tempo.minuto, tempo.secondo))

Per chiamare questa funzione occorre passare un oggetto Tempo come argomento:

>>> inizio = Tempo()
>>> inizio.ora = 9
>>> inizio.minuto = 45
>>> inizio.secondo = 00
>>> stampa_tempo(inizio)
09:45:00

Per trasformare stampa_tempo in un metodo, tutto quello che dobbiamo fare è spostare la definizione della funzione all’interno della definizione della classe. Notate bene la modifica nell’indentazione.

class Tempo:
    def stampa_tempo(tempo):
        print('%.2d:%.2d:%.2d' % (tempo.ora, tempo.minuto, tempo.secondo))

Ora ci sono due modi di chiamare stampa_tempo. Il primo (e meno usato) è utilizzare la sintassi delle funzioni:

>>> Tempo.stampa_tempo(inizio)
09:45:00

In questo uso della notazione a punto, Tempo è il nome della classe e stampa_tempo è il nome del metodo. inizio è passato come parametro.

Il secondo modo, più conciso, è usare la sintassi dei metodi:

>>> inizio.stampa_tempo()
09:45:00

Sempre usando la dot notation, stampa_tempo è ancora il nome del metodo, mentre inizio è l’oggetto sul quale il metodo è invocato, che è chiamato il soggetto. Come il soggetto di una frase è ciò a cui si riferisce la frase, il soggetto del metodo è ciò a cui si applica l’invocazione del metodo.

All’interno del metodo, il soggetto viene assegnato al primo dei parametri: in questo caso, inizio viene assegnato a tempo.

Per convenzione, il primo parametro di un metodo viene chiamato self, di conseguenza è bene riscrivere stampa_tempo così:

class Tempo:
    def stampa_tempo(self):
        print('%.2d:%.2d:%.2d' % (self.ora, self.minuto, self.secondo))

La ragione di questa convenzione è una metafora implicita:

  • La sintassi di una chiamata di funzione, stampa_tempo(inizio), suggerisce che la funzione è la parte attiva, che dice qualcosa del tipo: “Ehi, stampa_tempo! Ti passo un oggetto da stampare!”
  • Nella programmazione orientata agli oggetti, la parte attiva sono gli oggetti. L’invocazione di un metodo come inizio.stampa_tempo() dice: “Ehi, inizio! Stampa te stesso!”

Questo cambio di prospettiva sarà anche più elegante, ma cogliere la sua utilità non è immediato. Nei semplici esempi che abbiamo visto finora, può non esserlo. Ma in altri casi, spostare la responsabilità dalle funzioni agli oggetti rende possibile scrivere funzioni (o metodi) più versatili e rende più facile mantenere e riusare il codice.

Come esercizio, riscrivete tempo_in_int (vedere Paragrafo 16.4) come metodo. Potreste pensare di riscrivere anche int_in_tempo come metodo, ma non avrebbe molto senso: non vi sarebbe alcun oggetto sul quale invocarlo.

17.3  Un altro esempio

Ecco una versione di incremento (vedere Paragrafo 16.3), riscritto come metodo:

# all'interno della classe Tempo:

    def incremento(self, secondi):
        secondi += self.tempo_in_int()
        return int_in_tempo(secondi)

Questa versione presuppone che tempo_in_int sia stato scritto come metodo. Notate anche che si tratta di una funzione pura e non un modificatore.

Ecco come invocare incremento:

>>> inizio.stampa_tempo()
09:45:00
>>> fine = inizio.incremento(1337)
>>> fine.stampa_tempo()
10:07:17

Il soggetto, inizio, viene assegnato quale primo parametro, a self. L’argomento, 1337, viene assegnato quale secondo parametro, a secondi.

Questo meccanismo può confondere le idee, specie se commettete qualche errore. Per esempio, se invocate incremento con due argomenti ottenete:

>>> fine = inizio.incremento(1337, 460)
TypeError: incremento() takes 2 positional arguments but 3 were given

Il messaggio di errore a prima vista non è chiaro, perché ci sono solo due argomenti tra parentesi. Ma bisogna tener conto che anche il soggetto è considerato un argomento, ecco perché in totale fanno tre.

Tra parentesi, un argomento posizionale è un argomento privo di nome di un parametro; cioè, non è un argomento con nome. In questa chiamata di funzione:

sketch(pappagallo, gabbia, morto=True)

pappagallo e gabbia sono argomenti posizionali, e morto è un argomento con nome.

17.4  Un esempio più complesso

viene_dopo (vedere Paragrafo 16.1) è leggermente più complesso da riscrivere come metodo, perché richiede come parametri due oggetti Tempo. In questo caso, la convenzione prevede di denominare il primo parametro self e il secondo other:

# all'interno della classe Tempo:

    def viene_dopo(self, other):
        return self.tempo_in_int() > other.tempo_in_int()

Per usare questo metodo, lo dovete invocare su un oggetto e passare l’altro come argomento:

>>> fine.viene_dopo(inizio)
True

Una particolarità di questa sintassi è che si legge quasi come in italiano: “fine viene dopo inizio?”

17.5  Il metodo speciale init

Il metodo init (abbreviazione di initialization, ovvero inizializzazione) è un metodo speciale che viene invocato quando un oggetto viene istanziato. Il suo nome completo è __init__ (due caratteri underscore, seguiti da init, e da altri due underscore). Un metodo init per la classe Tempo può essere il seguente:

# all'interno della classe Tempo:

    def __init__(self, ora=0, minuto=0, secondo=0):
        self.ora = ora
        self.minuto = minuto
        self.secondo = secondo

È prassi che i parametri di __init__ abbiano gli stessi nomi degli attributi. L’istruzione

        self.ora = ora

memorizza il valore del parametro ora come attributo di self.

I parametri sono opzionali, quindi se chiamate Tempo senza argomenti, ottenete i valori di default.

>>> tempo = Tempo()
>>> tempo.stampa_tempo()
00:00:00

Se fornite un argomento, esso va a sovrascrivere ora:

>>> tempo = Tempo (9)
>>> tempo.stampa_tempo()
09:00:00

Se ne fornite due, sovrascrivono ora e minuto.

>>> tempo = Tempo(9, 45)
>>> tempo.stampa_tempo()
09:45:00

E se ne fornite tre, sovrascrivono tutti e tre i valori di default.

Per esercizio, scrivete un metodo init per la classe Punto che prenda x e y come parametri opzionali e li assegni agli attributi corrispondenti.

17.6  Il metodo speciale __str__

__str__ è un altro metodo speciale, come __init__, che ha lo scopo di restituire una rappresentazione di un oggetto in forma di stringa.

Ecco ad esempio un metodo str per un oggetto Tempo:

# all'interno della classe Tempo:

    def __str__(self):
        return '%.2d:%.2d:%.2d' % (self.ora, self.minuto, self.secondo)

Quando stampate un oggetto con l’istruzione di stampa, Python invoca il metodo str:

>>> tempo = Tempo(9, 45)
>>> print(tempo)
09:45:00

Personalmente, quando scrivo una nuova classe, quasi sempre inizio con lo scrivere __init__, che rende più facile istanziare un oggetto, e __str__, che è utile per il debugging.

Come esercizio, scrivete un metodo str per la classe Punto. Create un oggetto Punto e stampatelo.

17.7  Operator overloading

Nei tipi personalizzati, avete la possibilità di adattare il comportamento degli operatori attraverso la definizione di altri appositi metodi speciali. Per esempio se definite il metodo speciale di nome __add__ per la classe Tempo, potete poi usare l’operatore + sugli oggetti Tempo.

Ecco come potrebbe essere scritta la definizione:

# all'interno della classe Tempo:

    def __add__(self, other):
        secondi = self.tempo_in_int() + other.tempo_in_int()
        return int_in_tempo(secondi)

Ed ecco come può essere usata:

>>> inizio = Tempo(9, 45)
>>> durata = Tempo(1, 35)
>>> print(inizio + durata)
11:20:00

Quando applicate l’operatore + agli oggetti Tempo, Python invoca __add__. Quando stampate il risultato, Python invoca __str__. Accadono parecchie cose, dietro le quinte!

Cambiare il comportamento degli operatori in modo che funzionino con i tipi personalizzati è chiamato operator overloading (letteralmente, sovraccarico degli operatori). In Python, per ogni operatore esiste un corrispondente metodo speciale, come __add__. Per ulteriori dettagli consultate http://docs.python.org/2/reference/datamodel.html#specialnames.

Esercitatevi scrivendo un metodo add per la classe Punto.

17.8  Smistamento in base al tipo

Nel Paragrafo precedente abbiamo sommato due oggetti Tempo, ma potrebbe anche capitare di voler aggiungere un numero intero a un oggetto Tempo. Quella che segue è una versione di __add__ che controlla il tipo di other e, a seconda dei casi, invoca o somma_tempo o incremento:

# all'interno della classe Tempo:

    def __add__(self, other):
        if isinstance(other, Tempo):
            return self.somma_tempo(other)
        else:
            return self.incremento(other)

    def somma_tempo(self, other):
        secondi = self.tempo_in_int() + other.tempo_in_int()
        return int_in_tempo(secondi)

    def incremento(self, secondi):
        secondi += self.tempo_in_int()
        return int_in_tempo(secondi)

La funzione predefinita isinstance prende un valore e un oggetto classe, e restiutisce True se il valore è un’istanza della classe.

Quindi, se other è un oggetto Tempo, __add__ invoca somma_tempo. Altrimenti, considera che il parametro sia un numero, e invoca incremento. Questa operazione è detta smistamento in base al tipo, perché invia il calcolo a metodi diversi a seconda del tipo di argomento.

Ecco degli esempi che usano l’operatore + con tipi diversi:

>>> inizio = Tempo(9, 45)
>>> durata = Tempo(1, 35)
>>> print(inizio + durata)
11:20:00
>>> print(inizio + 1337)
10:07:17

Sfortunatamente, questa implementazione di addizione non è commutativa. Se l’intero è il primo operando vi risulterà infatti:

>>> print(1337 + inizio)
TypeError: unsupported operand type(s) for +: 'int' and 'instance'

Il problema è che, invece di chiedere all’oggetto Tempo di aggiungere un intero, Python chiede all’intero di aggiungere un oggetto Tempo, ma l’intero non ha la minima idea di come farlo. Ma a questo c’è una soluzione intelligente: il metodo speciale __radd__, che sta per right-side add (“addizione lato destro”). Questo metodo viene invocato quando un oggetto Tempo compare sul lato destro dell’operatore +. Eccone la definizione:

# all'interno della classe Tempo:

    def __radd__(self, other):
        return self.__add__(other)

Ed eccolo in azione:

>>> print(1337 + inizio)
10:07:17

Come esercizio, scrivete un metodo add per i Punti che possa funzionare sia con un oggetto Punto che con una tupla:

  • Se il secondo operando è un Punto, il metodo deve restituire un nuovo Punto la cui coordinata x sia la somma delle coordinate x cdegli operandi, e lo stesso per le coordinate y.
  • Se il secondo operando è una tupla, il metodo deve aggiungere il primo elemento della tupla alla coordinata x e il secondo elemento alla coordinata y, e restituire un nuovo Punto con le coordinate risultanti.

17.9  Polimorfismo

Lo smistamento in base al tipo è utile all’occorrenza, ma (fortunatamente) non è sempre necessario. Spesso potete evitarlo scrivendo le funzioni in modo che operino correttamente con argomenti di tipo diverso.

Molte delle funzioni che abbiamo scritto per le stringhe, funzioneranno anche con qualsiasi altro tipo di sequenza. Per esempio, nel Paragrafo 11.2 abbiamo usato istogramma per contare quante volte ciascuna lettera appare in una parola.

def istogramma(s):
    d = dict()
    for c in s:
        if c not in d:
            d[c] = 1
        else:
            d[c] = d[c]+1
    return d

Questa funzione è applicabile anche a liste, tuple e perfino dizionari, a condizione che gli elementi di s siano idonei all’hashing, in modo da poter essere usati come chiavi in d.

>>> t = ['spam', 'uovo', 'spam', 'spam', 'bacon', 'spam']
>>> istogramma(t)
{'bacon': 1, 'uovo': 1, 'spam': 4}

Le funzioni che sono in grado di operare con tipi diversi sono dette polimorfiche. Il polimorfismo facilita il riuso del codice. Ad esempio, la funzione predefinita sum, che addiziona gli elementi di una sequenza, funziona alla sola condizione che gli elementi della sequenza siano addizionabili.

Dato che agli oggetti Tempo abbiamo fornito un metodo add, funzionano con sum:

>>> t1 = Tempo(7, 43)
>>> t2 = Tempo(7, 41)
>>> t3 = Tempo(7, 37)
>>> totale = sum([t1, t2, t3])
>>> print(totale)
23:01:00

In linea generale, se tutte le operazioni all’interno di una funzione si possono applicare ad un dato tipo, la funzione può operare con quel tipo.

Il miglior genere di polimorfismo è quello involontario, quando scoprite che una funzione che avete già scritto può essere applicata anche ad un tipo che non avevate previsto.

17.10  Debug

È consentito aggiungere attributi in qualsiasi momento dell’esecuzione di un programma, ma se avete oggetti dello stesso tipo che non hanno gli stessi attributi, è facile generare errori. Inizializzare tutti gli attributi di un oggetto nel metodo init è considerata una prassi migliore.

Se non siete certi che un oggetto abbia un particolare attributo, potete usare la funzione predefinita hasattr (vedere Paragrafo 15.7).

Un altro modo di accedere agli attributi è la funzione predefinita vars, che prende un oggetto e restituisce un dizionario che fa corrispondere nomi degli attributi (come stringhe) e i relativi valori:

>>> p = Punto(3, 4)
>>> vars(p)
{'y': 4, 'x': 3}

Per gli scopi del debug, può essere utile tenere questa funzione a portata di mano:

def stampa_attributi(oggetto):
    for attr in vars(oggetto):
        print(attr, getattr(oggetto, attr))

stampa_attributi attraversa il dizionario e stampa ciascun nome di attributo con il suo valore.

La funzione predefinita getattr prende un oggetto e un nome di attributo (come stringa) e restituisce il valore dell’attributo.

17.11  Interfaccia e implementazione

Uno degli scopi della progettazione orientata agli oggetti è di rendere più agevole la manutenzione del software, che significa poter mantenere il programma funzionante quando altre parti del sistema vengono cambiate e poter modificare il programma per adeguarlo a dei nuovi requisiti.

Un principio di progettazione che aiuta a raggiungere questo obiettivo è di tenere le interfacce separate dalle implementazioni. Per gli oggetti, significa che i metodi esposti da una classe non devono dipendere da come vengono rappresentati gli attributi.

Per esempio, in questo capitolo abbiamo sviluppato una classe che rappresenta un’ora del giorno. I metodi esposti da questa classe comprendono tempo_in_int, viene_dopo, e somma_tempo.

Quei metodi possono essere implementati in diversi modi. I dettagli dell’implementazione dipendono da come rappresentiamo il tempo. In questo capitolo, gli attributi di un oggetto Tempo sono ora, minuto, e secondo.

Come alternativa, avremmo potuto sostituire quegli attributi con un singolo numero intero, come secondi trascorsi dalla mezzanotte. Con questa implementazione, alcuni metodi come viene_dopo, sarebbero diventati più facili da scrivere, ma altri più difficili.

Dopo aver sviluppato una nuova classe, potreste scoprire una implementazione migliore. Se altre parti del programma usano quella classe, cambiare l’interfaccia può essere dispendioso in termini di tempo e fonte di errori.

Ma se avete progettato l’interfaccia accuratamente, potete cambiare l’implementazione senza cambiare l’interfaccia, che significa che non occorre cambiare altre parti del programma.

17.12  Glossario

linguaggio orientato agli oggetti:
Linguaggio che possiede delle caratteristiche, come tipi personalizzati e metodi, che facilitano la programmazione orientata agli oggetti.
programmazione orientata agli oggetti:
Paradigma di programmazione in cui i dati e le operazioni sui dati vengono organizzati in classi e metodi.
metodo:
Funzione definita all’interno di una definizione di classe e che viene invocata su istanze di quella classe.
soggetto:
L’oggetto sul quale viene invocato un metodo.
argomento posizionale:
Un argomento che non include il nome di un parametro, ovvero non è un argomento con nome.
operator overloading:
Cambiare il comportamento di un operatore come + in modo che funzioni con un tipo personalizzato.
smistamento in base al tipo:
Schema di programmazione che controlla il tipo di un operando e invoca funzioni diverse in base ai diversi tipi.
polimorfico:
Di una funzione che può operare con più di un tipo di dati.
information hiding:
Principio per cui l’interfaccia di un oggetto non deve dipendere dalla sua implementazione, con particolare riferimento alla rappresentazione dei suoi attributi.

17.13  Esercizi


Esercizio 1  

Scaricate il codice degli esempi di questo capitolo (http://thinkpython2.com/code/Time2.py). Cambiate gli attributi di Tempo con un singolo intero che rappresenta i secondi dalla mezzanotte. Quindi modificate i metodi (e la funzione int_in_tempo) in modo che funzionino con la nuova implementazione. Non dovete cambiare il codice di prova in main. Quando avete finito, l’output dovrebbe essere lo stesso di prima. Soluzione: http://thinkpython2.com/code/Time2_soln.py.


Esercizio 2  

Questo esercizio è un aneddoto monitorio su uno degli errori più comuni e difficili da trovare in Python. Scrivete una definizione di una classe di nome Canguro con i metodi seguenti:

  1. Un metodo __init__ che inizializza un attributo di nome contenuto_tasca ad una lista vuota.
  2. Un metodo di nome intasca che prende un oggetto di qualsiasi tipo e lo inserisce in contenuto_tasca.
  3. Un metodo __str__ che restituisce una stringa di rappresentazione dell’oggetto Canguro e dei contenuti della tasca.

Provate il codice creando due oggetti Canguro, assegnandoli a variabili di nome can e guro, e aggiungendo poi guro al contenuto della tasca di can.

Scaricate http://thinkpython2.com/code/BadKangaroo.py. Contiene una soluzione al problema precedente, ma con un grande e serio errore. Trovatelo e sistematelo.

Se vi bloccate, potete scaricare http://thinkpython2.com/code/GoodKangaroo.py, che spiega il problema e illustra una soluzione.

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