Next Up Previous Hi Index

Chapter 14

Classi e metodi

14.1 Funzionalit� orientate agli oggetti

Python � un linguaggio di programmazione orientato agli oggetti il che significa che fornisce il supporto alla programmazione orientata agli oggetti.

Non � facile definire cosa sia la programmazione orientata agli oggetti ma abbiamo gi� visto alcune delle sue caratteristiche:

Per esempio la classe Tempo definita nel capitolo 13 corrisponde al modo in cui tendiamo a pensare alle ore del giorno e le funzioni che abbiamo definite corrispondono al genere di operazioni che facciamo con gli orari. Le classi Punto e Rettangolo sono estremamente simili ai concetti matematici corrispondenti.

Finora non ci siamo avvantaggiati delle funzionalit� di supporto della programmazione orientata agli oggetti fornite da Python. Sia ben chiaro che queste funzionalit� non sono indispensabili in quanto forniscono solo una sintassi alternativa per fare qualcosa che possiamo fare in modi pi� tradizionali, ma in molti casi questa alternativa � pi� concisa e accurata.

Per esempio nel programma Tempo non c'� una chiara connessione tra la definizione della classe e le definizioni di funzioni che l'hanno seguita: un esame superficiale � sufficiente per accorgersi che tutte queste funzioni prendono almeno un oggetto Tempo come parametro.

Questa osservazione giustifica la presenza dei metodi. Ne abbiamo gi� visto qualcuno nel caso dei dizionari, quando abbiamo invocato keys e values. Ogni metodo � associato ad una classe ed � destinato ad essere invocato sulle istanze di quella classe.

I metodi sono simili alle funzioni con due differenze:

Nelle prossime sezioni prenderemo le funzioni scritte nei due capitoli precedenti e le trasformeremo in metodi. Questa trasformazione � puramente meccanica e puoi farla seguendo una serie di semplici passi: se sei a tuo agio nel convertire tra funzione e metodo e viceversa riuscirai anche a scegliere di volta in volta la forma migliore.

14.2 StampaTempo

Nel capitolo 13 abbiamo definito una classe chiamata Tempo e scritto una funzione StampaTempo:

class Tempo:
  pass

def
StampaTempo(Orario):
  print str(Orario.Ore) + ":" +
        str(Orario.Minuti) + ":" +
        str(Orario.Secondi)

Per chiamare la funzione abbiamo passato un oggetto Tempo come parametro:

>>> OraAttuale = Tempo()
>>> OraAttuale.Ore = 9
>>> OraAttuale.Minuti = 14
>>> OraAttuale.Secondi = 30
>>> StampaTempo(OraAttuale)

Per rendere StampaTempo un metodo tutto quello che dobbiamo fare � muovere la definizione della funzione all'interno della definizione della classe. Fai attenzione al cambio di indentazione:

class Tempo:
  def StampaTempo(Orario):
    print str(Orario.Ore) + ":" +   \
          str(Orario.Minuti) + ":" +  \
          str(Orario.Secondi)

Ora possiamo invocare StampaTempo usando la notazione punto.

>>> OraAttuale.StampaTempo()

Come sempre l'oggetto su cui il metodo � invocato appare prima del punto ed il nome del metodo subito dopo.

L'oggetto su cui il metodo � invocato � automaticamente assegnato al primo parametro, quindi nel caso di OraAttuale � assegnato a Orario.

Per convenzione il primo parametro di un metodo � chiamato self, traducibile in questo caso come "l'oggetto stesso".

Come nel caso di StampaTempo(OraAttuale), la sintassi di una chiamata di funzione tradizionale suggerisce che la funzione sia l'agente attivo: equivale pressappoco a dire "StampaTempo! C'� un oggetto per te da stampare!"

Nella programmazione orientata agli oggetti sono proprio gli oggetti ad essere considerati l'agente attivo: un'invocazione del tipo OraAttuale.StampaTempo() significa "OraAttuale! Invoca il metodo per stampare il tuo valore!"

Questo cambio di prospettiva non sembra cos� utile ed effettivamente negli esempi che abbiamo visto finora � cos�. Comunque lo spostamento della responsabilit� dalla funzione all'oggetto rende possibile scrivere funzioni pi� versatili e rende pi� immediati il mantenimento ed il riutilizzo del codice.

14.3 Un altro esempio

Convertiamo Incremento (dalla sezione 13.3) da funzione a metodo. Per risparmiare spazio eviteremo di riscrivere il metodo StampaTempo che abbiamo gi� definito ma tu lo devi tenere nella tua versione del programma:

class Tempo:
  ...
  def Incremento(self, Secondi):
    self.Secondi = Secondi + self.Secondi

    while self.Secondi >= 60:
      self.Secondi = self.Secondi - 60
      self.Minuti = self.Minuti + 1

    while self.Minuti >= 60:
      self.Minuti = self.Minuti - 60
      self.Ore = self.Ore + 1

D'ora in poi i tre punti di sospensione ... all'interno del codice indicheranno che � stata omessa per questioni di leggibilit� una parte del codice gi� definito in precedenza.

La trasformazione, come abbiamo gi� detto, � puramente meccanica: abbiamo spostato la definizione di una funzione all'interno di una definizione di classe e cambiato il nome del primo parametro.

Ora possiamo invocare Incremento come metodo.

OraAttuale.Incremento(500)

Ancora una volta l'oggetto su cui il metodo � invocato viene automaticamente assegnato al primo parametro, self. Il secondo parametro, Secondi, vale 500.

Esercizio: converti ConverteInSecondi della sezione 13.5 a metodo della classe Tempo.

14.4 Un esempio pi� complesso

La funzione Dopo � leggermente pi� complessa perch� opera su due oggetti Tempo e non soltanto su uno com'� successo per i metodi appena visti. Uno dei parametri � chiamato self; l'altro non cambia:

class Tempo:
  ...
  def Dopo(self, Tempo2):
    if self.Ore > Tempo2.Ore:
      return 1
    if self.Ore < Tempo2.Ore:
      return 0

    if self.Minuti > Tempo2.Minuti:
      return 1
    if self.Minuti < Tempo2.Minuti:
      return 0

    if self.Secondi > Tempo2.Secondi:
      return 1
    return 0

Invochiamo questo metodo su un oggetto e passiamo l'altro come argomento:

if TempoCottura.Dopo(OraAttuale):
  print "Il pranzo e' pronto"

14.5 Argomenti opzionali

Abbiamo gi� visto delle funzioni predefinite che accettano un numero variabile di argomenti: string.find accetta due, tre o quattro argomenti.

Possiamo scrivere funzioni con una lista di argomenti opzionali. Scriviamo la nostra versione di Trova per farle fare la stessa cosa di string.find.

Ecco la versione originale che abbiamo scritto nella sezione 7.7:

def Trova(Stringa, Carattere):
  Indice = 0
  while Indice < len(Stringa):
    if Stringa[Indice] == Carattere:
      return Indice
    Indice = Indice + 1
  return -1

Questa � la versione aggiornata e migliorata:

def Trova(Stringa, Carattere, Inizio=0):
  Indice = Inizio
  while Inizio < len(Stringa):
    if Stringa[Indice] == Carattere:
      return Indice
    Indice = Indice + 1
  return -1

Il terzo parametro, Inizio, � opzionale perch� abbiamo fornito il valore 0 di default. Se invochiamo Trova con solo due argomenti usiamo il valore di default per il terzo cos� da iniziare la ricerca dall'inizio della stringa:

>>> Trova("Mela", "l")
2

Se forniamo un terzo parametro questo sovrascrive il valore di default:

>>> Trova("Mela", "l", 3)
-1

Esercizio: aggiungi un quarto parametro, Fine, che specifica dove interrompere la ricerca.

Attenzione: questo esercizio non � semplice come sembra. Il valore di default di Fine dovrebbe essere len(Stringa) ma questo non funziona. I valori di default sono valutati al momento della definizione della funzione, non quando questa � chiamata: quando Trova viene definita, Stringa non esiste ancora cos� non puoi conoscere la sua lunghezza. Trova un sistema per aggirare l'ostacolo.

14.6 Il metodo di inizializzazione

Il metodo di inizializzazione � un metodo speciale invocato quando si crea un oggetto. Il nome di questo metodo � __init__ (due caratteri di sottolineatura, seguiti da init e da altri due caratteri di sottolineatura). Un metodo di inizializzazione per la classe Tempo potrebbe essere:

class Tempo:
  def __init__(self, Ore=0, Minuti=0, Secondi=0):
    self.Ore = Ore
    self.Minuti = Minuti
    self.Secondi = Secondi

Non c'� conflitto tra l'attributo self.Ore e il parametro Ore. La notazione punto specifica a quale variabile ci stiamo riferendo.

Quando invochiamo il costruttore Tempo gli argomenti che passiamo sono girati a __init__:

>>> OraAttuale = Tempo(9, 14, 30)
>>> OraAttuale.StampaTempo()
>>> 9:14:30

Dato che i parametri sono opzionali possiamo anche ometterli:

>>> OraAttuale = Tempo()
>>> OraAttuale.StampaTempo()
>>> 0:0:0

Possiamo anche fornire solo il primo parametro:

>>> OraAttuale = Tempo(9)
>>> OraAttuale.StampaTempo()
>>> 9:0:0

o i primi due parametri:

>>> OraAttuale = Tempo(9, 14)
>>> OraAttuale.StampaTempo()
>>> 9:14:0

Infine possiamo anche passare un sottoinsieme dei parametri nominandoli esplicitamente:

>>> OraAttuale = Tempo(Secondi = 30, Ore = 9)
>>> OraAttuale.StampaTempo()
>>> 9:0:30

14.7 La classe Punto rivisitata

Riscriviamo la classe Punto che abbiamo gi� visto alla sezione 12.1 in uno stile pi� orientato agli oggetti:

class Punto:
  def __init__(self, x=0, y=0):
    self.x = x
    self.y = y

  def __str__(self):
    return '(' + str(self.x) + ', ' + str(self.y) + ')'

Il metodo di inizializzazione prende x e y come parametri opzionali. Il loro valore di default � 0.

Il metodo __str__ ritorna una rappresentazione di un oggetto Punto sotto forma di stringa. Se una classe fornisce un metodo chiamato __str__ questo sovrascrive il comportamento abituale della funzione str di Python.

>>> P = Punto(3, 4)
>>> str(P)
'(3, 4)'

La stampa di un oggetto Punto invoca __str__ sull'oggetto: la definizione di __str__ cambia dunque anche il comportamento di print:

>>> P = Punto(3, 4)
>>> print P
(3, 4)

Quando scriviamo una nuova classe iniziamo quasi sempre scrivendo __init__ (la funzione che rende pi� facile istanziare oggetti) e __str__ (utile per il debug).

14.8 Ridefinizione di un operatore

Alcuni linguaggi consentono di cambiare la definizione degli operatori predefiniti quando applicati a tipi definiti dall'utente. Questa caratteristica � chiamata ridefinizione dell'operatore (o "overloading dell'operatore") e si rivela molto utile soprattutto quando vogliamo definire nuovi tipi di operazioni matematiche.

Se vogliamo ridefinire l'operatore somma + scriveremo un metodo chiamato __add__:

class Punto:
  ...
  def __add__(self, AltroPunto):
    return Punto(self.x + AltroPunto.x, self.y + AltroPunto.y)

Come al solito il primo parametro � l'oggetto su cui � invocato il metodo. Il secondo parametro � chiamato AltroPunto per distinguerlo da self. Ora sommiamo due oggetti Punto restituendo la somma in un terzo oggetto Punto che conterr� la somma delle coordinate x e delle coordinate y.

Quando applicheremo l'operatore + ad oggetti Punto Python invocher� il metodo __add__:

>>>   P1 = Punto(3, 4)
>>>   P2 = Punto(5, 7)
>>>   P3 = P1 + P2
>>>   print P3
(8, 11)

L'espressione P1 + P2 � equivalente a P1.__add__(P2) ma ovviamente pi� elegante.

Esercizio: aggiungi il metodo __sub__(self, AltroPunto) che ridefinisca l'operatore sottrazione per la classe Punto.

Ci sono parecchi modi per ridefinire l'operatore moltiplicazione, aggiungendo il metodo __mul__ o __rmul__ o entrambi.

Se l'operatore a sinistra di * � un Punto Python invoca __mul__ assumendo che anche l'altro operando sia un oggetto di tipo Punto. In questo caso si dovr� calcolare il prodotto punto dei due punti secondo le regole dell'algebra lineare:

def __mul__(self, AltroPunto):
  return self.x * AltroPunto.x + self.y * AltroPunto.y

Se l'operando a sinistra di * � un tipo primitivo (e quindi diverso da un oggetto Punto) e l'operando a destra � di tipo Punto Python invocher� __rmul__ per calcolare una moltiplicazione scalare:

def __rmul__(self, AltroPunto):
  return Punto(AltroPunto * self.x,  AltroPunto * self.y)

Il risultato della moltiplicazione scalare � un nuovo punto le cui coordinate sono un multiplo di quelle originali. Se AltroPunto � un tipo che non pu� essere moltiplicato per un numero in virgola mobile __rmul__ produrr� un errore in esecuzione.

Questo esempio mostra entrambi i tipi di moltiplicazione:

>>> P1 = Punto(3, 4)
>>> P2 = Punto(5, 7)
>>> print P1 * P2
43
>>> print 2 * P2
(10, 14)

Cosa accade se proviamo a valutare P2 * 2? Dato che il primo parametro � un Punto Python invoca __mul__ con 2 come secondo argomento. All'interno di __mul__ il programma prova ad accedere la coordinata x di AltroPunto e questo tentativo genera un errore dato che un numero intero non ha attributi:

>>> print P2 * 2
AttributeError: 'int' object has no attribute 'x'

Questo messaggio d'errore � effettivamente troppo sibiliino per risultare di una qualche utilit�, e questo � ottimo esempio delle difficolt� che puoi incontrare nella programmazione ad oggetti: non � sempre semplice capire quale sia il codice che ha causato l'errore.

Per un trattato pi� esauriente sulla ridefinizione degli operatori vedi l'appendice B.

14.9 Polimorfismo

La maggior parte dei metodi che abbiamo scritto finora lavorano solo per un tipo specifico di dati. Quando crei un nuovo oggetto scrivi dei metodi che lavorano su oggetti di quel tipo.

Ci sono comunque operazioni che vorresti poter applicare a molti tipi come ad esempio le operazioni matematiche che abbiamo appena visto. Se pi� tipi di dato supportano lo stesso insieme di operazioni puoi scrivere funzioni che lavorano indifferentemente con ciascuno di questi tipi.

Per esempio l'operazione MoltSomma (comune in algebra lineare) prende tre parametri: il risultato � la moltiplicazione dei primi due e la successiva somma del terzo al prodotto. Possiamo scriverla cos�:

def MoltSomma(x, y, z):
  return x * y + z

Questo metodo lavorer� per tutti i valori di x e y che possono essere moltiplicati e per ogni valore di z che pu� essere sommato al prodotto.

Possiamo invocarla con valori numerici:

>>> MoltSomma(3, 2, 1)
7

o con oggetti di tipo Punto:

>>> P1 = Punto(3, 4)
>>> P2 = Punto(5, 7)
>>> print MoltSomma(2, P1, P2)
(11, 15)
>>> print MoltSomma(P1, P2, 1)
44

Nel primo caso il punto P1 � moltiplicato per uno scalare e il prodotto � poi sommato a un altro punto (P2). Nel secondo caso il prodotto punto produce un valore numerico al quale viene sommato un altro valore numerico.

Una funzione che accetta parametri di tipo diverso � chiamata polimorfica.

Come esempio ulteriore consideriamo il metodo DirittoERovescio che stampa due volte una stringa, prima direttamente e poi all'inverso:

def DirittoERovescio(Stringa):
  import copy
  Rovescio = copy.copy(Stringa)
  Rovescio.reverse()
  print str(Stringa) + str(Rovescio)

Dato che il metodo reverse � un modificatore si deve fare una copia della stringa prima di rovesciarla: in questo modo il metodo reverse non modificher� la lista originale ma solo una sua copia.

Ecco un esempio di funzionamento di DirittoERovescio con le liste:

>>>   Lista = [1, 2, 3, 4]
>>>   DirittoERovescio(Lista)
[1, 2, 3, 4][4, 3, 2, 1]

Era facilmente intuibile che questa funzione riuscisse a maneggiare le liste. Ma pu� lavorare con oggetti di tipo Punto?

Per determinare se una funzione pu� essere applicata ad un tipo nuovo applichiamo la regola fondamentale del polimorfismo:

Se tutte le operazioni all'interno della funzione possono essere applicate ad un tipo di dato allora la funzione stessa pu� essere applicata al tipo.

Le operazioni nel metodo DirittoERovescio includono copy, reverse e print.

copy funziona su ogni oggetto e abbiamo gi� scritto un metodo __str__ per gli oggetti di tipo Punto cos� l'unica cosa che ancora ci manca � il metodo reverse:

def reverse(self):
  self.x , self.y = self.y, self.x

Ora possiamo passare Punto a DirittoERovescio:

>>>   P = Punto(3, 4)
>>>   DirittoERovescio(P)
(3, 4)(4, 3)

Il miglior tipo di polimorfismo � quello involontario, quando scopri che una funzione gi� scritta pu� essere applicata ad un tipo di dati per cui non era stata pensata.

14.10 Glossario

Linguaggio orientato agli oggetti
linguaggio che � dotato delle caratteristiche che facilitano la programmazione orientata agli oggetti, tipo la possibilit� di definire classi e l'ereditariet�.
Programmazione orientata agli oggetti
stile di programmazione nel quale i dati e le operazioni che li manipolano sono organizzati in classi e metodi.
Metodo
funzione definita all'interno di una definizione di classe invocata su istanze di quella classe.
Ridefinire
rimpiazzare un comportamento o un valore di default, scrivendo un metodo con lo stesso nome o rimpiazzando un parametro di default con un valore particolare.
Metodo di inizializzazione
metodo speciale invocato automaticamente nel momento in cui viene creato un nuovo oggetto e usato per inizializzare gli attributi dell'oggetto stesso.
Ridefinizione dell'operatore
estensione degli operatori predefiniti (+, -, *, >, <, ecc.) per farli lavorare con i tipi definiti dall'utente.
Prodotto punto
operazione definita nell'algebra lineare che moltiplica due punti e produce un valore numerico.
Moltiplicazione scalare
operazione definita nell'algebra lineare che moltiplica ognuna delle coordinate di un punto per un valore numerico.
Funzione polimorfica
funzione che pu� operare su pi� di un tipo di dati. Se tutte le operazioni in una funzione possono essere applicate ad un tipo di dato allora la funzione pu� essere applicata al tipo.


Next Up Previous Hi Index