Lezione 13 Python Classi e Metodi

      Nessun commento su Lezione 13 Python Classi e Metodi

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:

  • I programmi sono costituiti da definizioni di oggetti e definizioni di funzioni e la gran parte dell’elaborazione è espressa in termini di operazioni sugli oggetti.
  • Ogni definizione di oggetto corrisponde ad un oggetto o concetto del mon- do reale e le funzioni che operano su un oggetto corrispondono a modi reali di interazione tra cose reali.

Per esempio la classe Tempo definita nella lezione precedente 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. La classe Punto è estremamente simile al concetti matematici corrispondente.

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:

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

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.

StampaTempo


Nella lezione precedente abbiamo definito una classe chiamata Tempo e accennato ad 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.

Il metodo di inizializzazione

Il metodo di inizializzazione è un metodo speciale invocato quando si crea un oggetto. Il nome di questo metodo è page153image13400 page153image13600__init__ page153image13832 page153image14032(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

La classe Punto rivisitata
Riscriviamo la classe Punto che abbiamo già visto nella lezione precedente 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 page154image12224 page154image12424__str__ page154image12656 page154image12856ritorna una rappresentazione di un oggetto Punto sotto forma di stringa. Se una classe fornisce un metodo chiamato page154image13856 __page154image14056str__ page154image14288 page154image14488questo sovrascrive il comportamento abituale della funzione str di Python.

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

La stampa di un oggetto Punto invoca page155image2920 page155image3120__str__ page155image3352 page155image3552sull’oggetto: la definizione di __str__ page155image4088 page155image4288cambia dunque anche il comportamento di print:
>>> P = Punto(3, 4)
>>> print P
(3, 4)

Quando scriviamo una nuova classe iniziamo quasi sempre scrivendo page155image6128 page155image6328__init__ (la funzione che rende più facile istanziare oggetti) e page155image7088 __page155image7288str__ page155image7520 page155image7720(utile per il debug).

 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 es- sere 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 page158image3776 __str__ page158image4208 page158image4408per 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.