La programmazione orientata agli oggetti permette al programmatore di creare nuovi tipi di dato che si comportano come quelli predefiniti. Esploreremo questa capacità costruendo una classe Frazione che possa lavorare come i tipi di dato numerico predefiniti (intero, intero lungo e virgola mobile).
Le frazioni, conosciute anche come numeri razionali, sono numeri che si possono esprimere come rapporto tra numeri interi, come nel caso di 5/6. Il numero superiore si chiama numeratore, quello inferiore denominatore.
Iniziamo con una definizione della classe Frazione con un metodo di inizializzazione che fornisce un numeratore ed un denominatore interi.
class Frazione:
def __init__(self, Numeratore, Denominatore=1):
self.Numeratore = Numeratore
self.Denominatore = Denominatore
Il denominatore è opzionale: se la frazione è creata (istanziata) con un solo parametro rappresenta un intero così che se il numeratore è n costruiremo la frazione n/1.
Il prossimo passo è quello di scrivere il metodo __str__ per stampare le frazioni in un modo che sia comprensibile. La forma "numeratore/denominatore" è probabilmente quella più "naturale":
class Frazione:
...
def __str__(self):
return "%d/%d" % (self.Numeratore, self.Denominatore)
Per testare ciò che abbiamo fatto finora scriviamo tutto in un file chiamato frazione.py (o qualcosa di simile; l'importante è che per te abbia un nome che ti permetta di rintracciarlo in seguito) e lo importiamo nell'interprete Python. Poi passiamo a creare un oggetto Frazione e a stamparlo:
>>> from Frazione import Frazione
>>> f = Frazione(5,6)
>>> print "La frazione e'", f
La frazione e' 5/6
Come abbiamo già visto il comando print invoca il metodo __str__ implicitamente.
Ci interessa poter applicare le consuete operazioni matematiche a operandi di tipo Frazione. Per farlo procediamo con la ridefinizione degli operatori matematici quali l'addizione, la sottrazione, la moltiplicazione e la divisione.
Iniziamo dalla moltiplicazione perché è la più semplice da implementare. Il risultato della moltiplicazione di due frazioni è una frazione che ha come numeratore il prodotto dei due numeratori, e come denominatore il prodotto dei denominatori. __mul__ è il nome usato da Python per indicare l'operatore *:
class Frazione:
...
def __mul__(self, Altro):
return Frazione(self.Numeratore * Altro.Numeratore,
self.Denominatore * Altro.Denominatore)
Possiamo testare subito questo metodo calcolando il prodotto di due frazioni:
>>> print Frazione(5,6) * Frazione(3,4)
15/24
Funziona, ma possiamo fare di meglio. Possiamo infatti estendere il metodo per gestire la moltiplicazione di una frazione per un intero, usando la funzione type per controllare se Altro è un intero. In questo caso prima di procedere con la moltiplicazione lo si convertirà in frazione:
class Frazione:
...
def __mul__(self, Altro):
if type(Altro) == type(5):
Altro = Frazione(Altro)
return Frazione(self.Numeratore * Altro.Numeratore,
self.Denominatore * Altro.Denominatore)
La moltiplicazione tra frazioni e interi ora funziona, ma solo se la frazione compare alla sinistra dell'operatore:
>>> print Frazione(5,6) * 4
20/6
>>> print 4 * Frazione(5,6)
TypeError: unsupported operand type(s) for *: 'int' and 'instance
Per valutare un operatore binario come la moltiplicazione Python controlla l'operando di sinistra per vedere se questo fornisce un metodo __mul__ che supporta il tipo del secondo operando. Nel nostro caso l'operatore moltiplicazione predefinito per gli interi non supporta le frazioni (com'è giusto, dato che abbiamo appena inventato noi la classe Frazione).
Se il controllo non ha successo Python passa a controllare l'operando di destra per vedere se è stato definito un metodo __rmul__ che supporta il tipo di dato dell'operatore di sinistra. Visto che non abbiamo ancora scritto __rmul__ il controllo fallisce e viene mostrato il messaggio di errore.
Esiste comunque un metodo molto semplice per scrivere __rmul__:
class Frazione:
...
__rmul__ = __mul__
Con questa assegnazione diciamo che il metodo __rmul__ è lo stesso di __mul__, così che per valutare 4 * Fraction(5,6) Python invoca __rmul__ sull'oggetto Frazione e passa 4 come parametro:
>>> print 4 * Frazione(5,6)
20/6
Dato che __rmul__ è lo stesso di __mul__ e che quest'ultimo accetta parametri interi è tutto a posto.
L'addizione è più complessa della moltiplicazione ma non troppo: la somma di a/b e c/d è infatti la frazione (a*d+c*b)/b*d.
Usando il codice della moltiplicazione come modello possiamo scrivere __add__ e __radd__:
class Frazione:
...
def __add__(self, Altro):
if type(Altro) == type(5):
Altro = Frazione(Altro)
return Fraction(self.Numeratore * Altro.Denominatore +
self.Denominatore * Altro.Numeratore,
self.Denominatore * Altro.Denominatore)
__radd__ = __add__
Possiamo testare questi metodi con frazioni e interi:
>>> print Frazione(5,6) + Frazione(5,6)
60/36
>>> print Frazione(5,6) + 3
23/6
>>> print 2 + Frazione(5,6)
17/6
I primi due esempi invocano __add__; l'ultimo __radd__.
Nell'esempio precedente abbiamo calcolato la somma 5/6 + 5/6 e ottenuto 60/36. Il risultato è corretto ma quella ottenuta non è la sua migliore rappresentazione. Per ridurre la frazione ai suoi termini più semplici dobbiamo dividere il numeratore ed il numeratore per il loro massimo comune divisore (MCD) che è 12. Il risultato diventa quindi 5/3.
In generale quando creiamo e gestiamo un oggetto Frazione dovremmo sempre dividere numeratore e denominatore per il loro MCD. Nel caso di una frazione già ridotta il MCD è 1.
Euclide di Alessandria (circa 325--265 A.C.) inventò un algoritmo per calcolare il massimo comune divisore tra due numeri interi m e n:
Se n divide perfettamente m allora il MCD è n. In caso contrario il MCD è il MCD tra n ed il resto della divisione di m diviso per n.
Questa definizione ricorsiva può essere espressa in modo conciso con una funzione:
def MCD(m, n):
if m % n == 0:
return n
else:
return MCD(n, m%n)
Nella prima riga del corpo usiamo l'operatore modulo per controllare la divisibilità. Nell'ultima riga lo usiamo per calcolare il resto della divisione.
Dato che tutte le operazioni che abbiamo scritto finora creano un nuovo oggetto Frazione come risultato potremmo inserire la riduzione nel metodo di inizializzazione:
class Frazione:
def __init__(self, Numeratore, Denominatore=1):
mcd = MCD(numeratore, Denominatore)
self.Numeratore = Numeratore / mcd
self.Denominatore = Denominatore / mcd
Quando creiamo una nuova Frazione questa sarà immediatamente ridotta alla sua forma più semplice:
>>> Frazione(100,-36)
-25/9
Una bella caratteristica di MCD è che se la frazione è negativa il segno meno è sempre spostato automaticamente al numeratore.
Supponiamo di dover confrontare due oggetti di tipo Frazione, a e b valutando a == b. L'implementazione standard di == ritorna vero solo se a e b sono lo stesso oggetto, effettuando un confronto debole.
Nel nostro caso vogliamo probabilmente ritornare vero se a e b hanno lo stesso valore e cioè fare un confronto forte. Ne abbiamo già parlato nella sezione 12.4.
Dobbiamo quindi insegnare alle frazioni come confrontarsi tra di loro. Come abbiamo visto nella sezione 15.4, possiamo ridefinire tutti gli operatori di confronto in una volta sola fornendo un nuovo metodo __cmp__.
Per convenzione il metodo __cmp__ ritorna un numero negativo se self è minore di Altro, zero se sono uguali e un numero positivo se self è più grande di Altro.
Il modo più semplice per confrontare due frazioni è la moltiplicazione incrociata: se a/b > c/d allora ad > bc. Con questo in mente ecco quindi il codice per __cmp__:
class Frazione:
...
def __cmp__(self, Altro):
Differenza = (self.Numeratore * Altro.Denominatore -
Altro.Numeratore * self.Denominatore)
return Differenza
Se self è più grande di Altro allora Differenza è positiva. Se Altro è maggiore allora Differenza è negativa. Se sono uguali Differenza è zero.
Logicamente non abbiamo ancora finito. Dobbiamo ancora implementare la sottrazione ridefinendo __sub__ e la divisione con il corrispondente metodo __div__.
Un modo per gestire queste operazioni è quello di implementare la negazione ridefinendo __neg__ e l'inversione con __invert__: possiamo infatti sottrarre sommando al primo operando la negazione del secondo, e dividere moltiplicando il primo operando per l'inverso del secondo. Poi dobbiamo fornire __rsub__ e __rdiv__.
Purtroppo non possiamo usare la scorciatoia già vista nel caso di addizione e moltiplicazione dato che sottrazione e divisione non sono commutative. Non possiamo semplicemente assegnare __rsub__ e __rdiv__ a lle corrispondenti __sub__ e __div__, dato che in queste operazioni l'ordine degli operandi fa la differenza...
Per gestire la negazione unaria, che non è altro che l'uso del segno meno con un singolo operando (da qui il termine "unaria" usato nella definizione), sarà necessario ridefinire il metodo __neg__.
Potremmo anche calcolare le potenze ridefinendo __pow__ ma l'implementazione in questo caso è un po' complessa: se l'esponente non è un intero, infatti, può non essere possibile rappresentare il risultato come Frazione. Per fare un esempio, Frazione(2) ** Frazione(1,2) non è nient'altro che la radice di 2 che non è un numero razionale e quindi non può essere rappresentato come frazione. Questo è il motivo per cui non è così facile scrivere una versione generale di __pow__.
C'è un'altra estensione della classe Frazione che potrebbe rivelarsi utile: finora siamo partiti dal presupposto che numeratore e denominatore sono interi, ma nulla ci vieta di usare interi lunghi.
Esercizio: completa l'implementazione della classe Frazione per gestire sottrazione, divisione ed elevamento a potenza, con interi lunghi al numeratore e denominatore.