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