La caratteristica più frequentemente associata alla programmazione ad oggetti è l'ereditarietà che è la capacità di definire una nuova classe come versione modificata di una classe già esistente.
Il vantaggio principale dell'ereditarietà è che si possono aggiungere nuovi metodi ad una classe senza dover modificare la definizione originale. È chiamata "ereditarietà" perché la nuova classe "eredita" tutti i metodi della classe originale. Estendendo questa metafora la classe originale è spesso definita "genitore" e la classe derivata "figlia" o "sottoclasse".
L'ereditarietà è una caratteristica potente e alcuni programmi possono essere scritti in modo molto più semplice e conciso grazie ad essa, dando inoltre la possibilità di personalizzare il comportamento di una classe senza modificare l'originale. Il fatto stesso che la struttura dell'ereditarietà possa riflettere quella del problema può rendere in qualche caso il programma più semplice da capire.
D'altro canto l'ereditarietà può rendere più difficile la lettura del programma, visto che quando si invoca un metodo non è sempre chiaro dove questo sia stato definito (se all'interno del genitore o delle classi da questo derivate) con il codice che deve essere rintracciato all'interno di più moduli invece che essere in un unico posto ben definito. Molte delle cose che possono essere fatte con l'ereditarietà possono essere di solito gestite elegantemente anche senza di essa, ed è quindi il caso di usarla solo se la struttura del problema la richiede: se usata nel momento sbagliato può arrecare più danni che apportare benefici.
In questo capitolo mostreremo l'uso dell'ereditarietà come parte di un programma che gioca a Old Maid, un gioco di carte piuttosto meccanico e semplice. Anche se implementeremo un gioco particolare uno dei nostri scopi è quello di scrivere del codice che possa essere riutilizzato per implementare altri tipi di giochi di carte.
Per la maggior parte dei giochi di carte abbiamo la necessità di rappresentare una mano di carte. La mano è simile al mazzo, dato che entrambi sono insiemi di carte e richiedono metodi per aggiungere e rimuovere carte. Inoltre abbiamo bisogno sia per la mano che per il mazzo di poter mescolare le carte.
La mano si differenzia dal mazzo perché, a seconda del gioco, possiamo avere la necessità di effettuare su una mano alcuni tipi di operazioni che per un mazzo non avrebbero senso: nel poker posso avere l'esigenza di classificare una mano (full, colore, ecc.) o confrontarla con un'altra mano mentre nel bridge devo poter calcolare il punteggio di una mano per poter effettuare una puntata.
Questa situazione suggerisce l'uso dell'ereditarietà: se creiamo Mano come sottoclasse di Mazzo avremo immediatamente disponibili tutti i metodi di Mazzo con la possibilità di riscriverli o di aggiungerne altri.
Nella definizione della classe figlia il nome del genitore compare tra parentesi:
class Mano(Mazzo):
pass
Questa istruzione indica che la nuova classe Mano eredita dalla classe già esistente Mazzo.
Il costruttore Mano inizializza gli attributi della mano, che sono il Nome e le Carte. La stringa Nome identifica la mano ed è probabilmente il nome del giocatore che la sta giocando: è un parametro opzionale che per default è una stringa vuota. Carte è la lista delle carte nella mano, inizializzata come lista vuota:
class Mano(Mazzo):
def __init__(self, Nome=""):
self.Carte = []
self.Nome = Nome
In quasi tutti i giochi di carte è necessario poter aggiungere e rimuovere carte dalla mano. Della rimozione ce ne siamo già occupati, dato che Mano eredita immediatamente RimuoviCarta da Mazzo. Dobbiamo invece scrivere AggiungeCarta:
class Mano(Mazzo):
def __init__(self, Nome=""):
self.Carte = []
self.Nome = Nome
def AggiungeCarta(self,Carta) :
self.Carte.append(Carta)
Il metodo di lista append aggiunge una nuova carta alla fine della lista di carte.
Ora che abbiamo una classe Mano vogliamo poter spostare delle carte dal Mazzo alle singole mani. Non è immediatamente ovvio se questo metodo debba essere inserito nella classe Mano o nella classe Mazzo ma dato che opera su un mazzo singolo e (probabilmente) su più mani è più naturale inserirlo in Mazzo.
Il metodo Distribuisci dovrebbe essere abbastanza generale da poter essere usato in vari giochi e deve permettere la distribuzione tanto dell'intero mazzo che di una singola carta.
Distribuisci prende due argomenti: una lista (o tupla) di mani e il numero totale di carte da distribuire. Se non ci sono carte sufficienti per la distribuzione il metodo distribuisce quelle in suo possesso e poi si ferma:
class Mazzo:
...
def Distribuisci(self, ListaMani, NumCarte=999):
NumMani = len(ListaMani)
for i in range(NumCarte):
if self.EVuoto(): break # si ferma se non ci sono
# ulteriori carte
Carta = self.PrimaCarta() # prende la carta superiore
# del mazzo
Mano = ListaMani[i % NumMani] # di chi e' il prossimo
# turno?
Mano.AggiungeCarta(Carta) # aggiungi la carta alla
# mano
Il secondo parametro, NumCarte, è opzionale; il valore di default è molto grande per essere certi che vengano distribuite tutte le carte del mazzo.
La variabile del ciclo i va da 0 a NumCarte-1. Ogni volta che viene eseguito il corpo del ciclo, la prima carta del mazzo viene rimossa usando il metodo di lista pop che rimuove e ritorna l'ultimo valore di una lista.
L'operatore modulo (%) ci permette di distribuire le carte in modo corretto, una carta alla volta per ogni mano: quando i è uguale al numero delle mani nella lista l'espressione i % NumMani restituisce 0 permettendo di ricominciare dal primo elemento della lista delle mani.
Per stampare il contenuto di una mano possiamo avvantaggiarci dei metodi StampaMazzo e __str__ ereditati da Mazzo. Per esempio:
>>> Mazzo1 = Mazzo()
>>> Mazzo1.Mescola()
>>> Mano1 = Mano("pippo")
>>> Mazzo1.Distribuisci([Mano1], 5)
>>> print Mano1
2 di Picche
3 di Picche
4 di Picche
Asso di Cuori
9 di Fiori
Anche se è comodo ereditare da metodi esistenti può essere necessario modificare il metodo __str__ nella classe Mano per aggiungere qualche informazione, ridefinendo il metodo omonimo ereditato dalla classe Mazzo:
class Mano(Mazzo)
...
def __str__(self):
s = "La mano di " + self.Nome
if self.EVuoto():
s = s + " e' vuota\n"
else:
s = s + " contiene queste carte:\n"
return s + Mazzo.__str__(self)
s è una stringa che inizialmente indica chi è il proprietario della mano. Se la mano è vuota vengono aggiunte ad s le parole "e' vuota" e viene ritornata s. IN caso contrario vengono aggiunte le parole "contiene queste carte" e la rappresentazione della mano sotto forma di stringa già vista in Mazzo, elaborata invocando il metodo __str__ della classe Mazzo su self.
Potrebbe sembrarti strano il fatto di usare self, che si riferisce alla mano corrente, con un metodo appartenente alla classe Mazzo: ricorda che Mano è un tipo di Mazzo. Gli oggetti Mano possono fare qualsiasi cosa di cui è capace Mazzo e così è legale invocare un metodo Mazzo con la mano self.
In genere è sempre legale usare un'istanza di una sottoclasse invece di un'istanza della classe genitore.
La classe GiocoDiCarte si occupa delle operazioni comuni in tutti i giochi di carte, quali possono essere la creazione del mazzo ed il mescolamento delle sue carte:
class GiocoDiCarte:
def __init__(self):
self.Mazzo = Mazzo()
self.Mazzo.Mescola()
In questo primo caso abbiamo visto come il metodo di inizializzazione non si limiti ad assegnare dei valori agli attributi, ma esegua una elaborazione significativa.
Per implementare dei giochi specifici possiamo successivamente ereditare da GiocoDiCarte e aggiungere a questa classe le caratteristiche del nuovo gioco. Per fare un esempio scriveremo una simulazione di Old Maid.
L'obiettivo di Old Maid è quello di riuscire a sbarazzarsi di tutte le carte che si hanno in mano. Questo viene fatto eliminando coppie di carte che hanno lo stesso rango e colore: il 4 di fiori viene eliminato con il 4 di picche perché entrambi i segni sono neri; il jack di cuori con il jack di quadri perché entrambi sono rossi.
Per iniziare il gioco la Regina di Fiori è tolta dal mazzo per fare in modo che la Regina di Picche non possa essere eliminata durante la partita. Le 51 carte sono poi tutte distribuite una alla volta in senso orario ai giocatori e dopo la distribuzione tutti i giocatori scartano immediatamente quante più carte possibili eliminando le coppie presenti nella mano appena distribuita.
Quando non si possono più scartare carte il gioco ha inizio. A turno ogni giocatore pesca senza guardarla una carta dal giocatore che, in senso orario, ha ancora delle carte in mano. Se la carta scelta elimina una carta in mano la coppia viene rimossa. In caso contrario la carta scelta rimane in mano.
Alla fine della partita tutte le eliminazioni saranno state fatte ed il perdente è chi rimane con la Regina di Picche in mano.
Nella nostra simulazione del gioco il computer giocherà tutte le mani. Sfortunatamente alcune sottigliezze del gioco verranno perse: nel gioco reale chi si trova in mano la Regina di Picche farà di tutto per fare in modo che questa venga scelta da un vicino, disponendola in modo da facilitare un successo in tal senso. Il computer invece sceglierà le carte completamente a caso.
Una mano per giocare a Old Maid richiede alcune capacità che vanno oltre rispetto a quelle fornite da Mano. Sarà opportuno quindi definire una nuova classe ManoOldMaid, che erediterà i metodi da Mano e a questi metodi ne verrà aggiunto uno (RimuoveCoppie) per rimuovere le coppie di carte:
class ManoOldMaid(Mano):
def RimuoveCoppie(self):
Conteggio = 0
CarteOriginali = self.Carte[:]
for CartaOrig in CarteOriginali:
CartaDaCercare = Carta(3-CartaOrig.Seme, CartaOrig.Rango)
if CartaDaCercare in self.Carte:
self.Carte.remove(CartaOrig)
self.Carte.remove(CartaDaCercare)
print "Mano di %s : %s elimina %s" %
(self.Nome,CartaOrig,CartaDaCercare)
Conteggio = Conteggio + 1
return Conteggio
Iniziamo facendo una copia della lista di carte, così da poter attraversare la copia finché non rimuoviamo l'originale: dato che self.Carte viene modificata durante l'attraversamento, non possiamo di certo usarla per controllare tutti i suoi elementi. Python potrebbe essere confuso dal fatto di veder cambiare la lista che sta attraversando!
Per ogni carta della mano andiamo a controllare se quella che la elimina è presente nella stessa mano. La carta "eliminante" ha lo stesso rango e l'altro seme dello stesso colore di quella "eliminabile": l'espressione 3-Carta.Seme serve proprio a trasformare una carta di Fiori (seme 0) in Picche (seme 3) e viceversa; una carta di Quadri (seme 1) in Cuori (seme 2) e viceversa.
Se entrambe le carte sono presenti sono rimosse con RimuoveCoppie:
>>> Partita = GiocoDiCarte()
>>> Mano1 = ManoOldMaid("Franco")
>>> Partita.Mazzo.Mescola([Mano1], 13)
>>> print Mano1
La mano di Franco contiene queste carte:
Asso di Picche
2 di Quadri
7 di Picche
8 di Fiori
6 di Cuori
8 di Picche
7 di Fiori
Regina di Fiori
7 di Quadri
5 di Fiori
Jack di Quadri
10 di Quadri
10 di Cuori
>>> Mano1.RimuoveCoppie()
Mano di Franco: 7 di Picche elimina 7 di Fiori
Mano di Franco: 8 di Picche elimina 8 di Fiori
Mano di Franco: 10 di Quadri elimina 10 di Cuori
>>> print Mano1
La mano di Franco contiene queste carte:
Asso di Picche
2 di Quadri
6 di Cuori
Regina di Fiori
7 di Quadri
5 di Fiori
Jack di Quadri
Nota che non c'è un metodo di inizializzazione __init__ per la classe
ManoOldMaid dato che l'abbiamo ereditato da Mano.
Ora possiamo dedicarci al gioco vero e proprio: GiocoOldMaid è una sottoclasse di GiocoDiCarte con un metodo Giocatori che prende una lista di giocatori come parametro.
Dato che __init__ è ereditato da GiocoDiCarte un nuovo oggetto GiocoOldMaid contiene un mazzo già mescolato:
class GiocoOldMaid(GiocoDiCarte):
def Partita(self, Nomi):
# rimozione della regina di fiori
self.Mazzo.RimuoviCarta(Carta(0,12))
# creazione di una mano per ogni giocatore
self.Mani = []
for Nome in Nomi:
self.Mani.append(ManoOldMaid(Nome))
# distribuzione delle carte
self.Mazzo.Distribuisci(self.Mani)
print "---------- Le carte sono state distribuite"
self.StampaMani()
# toglie le coppie iniziali
NumCoppie = self.RimuoveTutteLeCoppie()
print "---------- Coppie scartate, inizia la partita"
self.StampaMani()
# gioca finche' non sono state fatte 25 coppie
Turno = 0
NumMani = len(self.Mani)
while NumCoppie < 25:
NumCoppie = NumCoppie + self.GiocaUnTurno(Turno)
Turno = (Turno + 1) % NumMani
print "---------- La partita e' finita"
self.StampaMani()
Alcuni dei passi della partita sono stati separati in metodi singoli per ragioni di chiarezza anche se dal punto di vista del programma questo non era strettamente necessario.
RimuoveTutteLeCoppie attraversa la lista di mani e invoca RimuoveCoppie su ognuna:
class GiocoOldMaid(GiocoDiCarte):
...
def RimuoveTutteLeCoppie(self):
Conteggio = 0
for Mano in self.Mani:
Conteggio = Conteggio + Mano.RimuoveCoppie()
return Conteggio
Esercizio: scrivi StampaMani che attraversa self.Mani e stampa ciascuna mano.
Conteggio è un accumulatore che tiene traccia del numero di coppie rimosse dall'inizio della partita: quando il numero totale di coppie raggiunge 25 sono state rimosse dalle mani esattamente 50 carte, e ciò significa che è rimasta solo una carta (la Regina di Picche) ed il gioco è finito.
La variabile Turno tiene traccia di quale giocatore debba giocare. Parte da 0 e viene incrementata di 1 ad ogni mano. Quando arriva a NumMani l'operatore modulo % la riporta a 0.
Il metodo GiocaUnTurno prende un parametro dal giocatore che sta giocando. Il valore ritornato è il numero di coppie rimosse durante il turno:
class GiocoOldMaid(GiocoDiCarte):
...
def GiocaUnTurno(self, Giocatore):
if self.Mani[Giocatore].EVuoto():
return 0
Vicino = self.TrovaVicino(Giocatore)
CartaScelta = self.Mani[Vicino].PrimaCarta()
self.Mani[Giocatore].AggiungeCarta(CartaScelta)
print "Mano di", self.Mani[Giocatore].Nome, \
": scelta", CartaScelta
Conteggio = self.Mani[Giocatore].RimuoveCoppie()
self.Mani[Giocatore].Mescola()
return Conteggio
Se la mano di un giocatore è vuota quel giocatore è fuori dal gioco e non fa nulla. Il valore di ritorno in questo caso è 0.
In caso contrario un turno consiste nel trovare il primo giocatore in senso orario che abbia delle carte in mano, prendergli una carta e cercare coppie da rimuovere dopo avere aggiunto la carta scelta alla mano. Prima di tornare le carte in mano devono essere mescolate così che la scelta del prossimo giocatore sia ancora una volta casuale.
Il metodo TrovaVicino inizia con il giocatore all'immediata sinistra e continua in senso orario finché non trova qualcuno che ha ancora carte in mano:
class GiocoOldMaid(GiocoDiCarte):
...
def TrovaVicino(self, Giocatore):
NumMani = len(self.Mani)
for Prossimo in range(1,NumMani):
Vicino = (Giocatore + Prossimo) % NumMani
if not self.Mani[Vicino].EVuoto():
return Vicino
Se TrovaVicino dovesse effettuare un giro completo dei giocatori senza trovare qualcuno con delle carte in mano tornerebbe None e causerebbe un errore da qualche parte del programma. Fortunatamente possiamo provare che questo non succederà mai, sempre che la condizione di fine partita sia riconosciuta correttamente.
Abbiamo omesso il metodo StampaMani dato che puoi scriverlo tu senza problemi.
La stampa che mostriamo in seguito mostra una partita effettuata usando le sole quindici carte di valore più elevato (i 10, i jack, le regine ed i re), ed è stata ridotta per questioni di spazio. La partita ha visto come protagonisti tre giocatori: Allen, Jeff e Chris. Con un mazzo così piccolo il gioco si ferma dopo aver rimosso 7 coppie invece delle consuete 25.
>>> import Carte
>>> Gioco = Carte.GiocoOldMaid()
>>> Gioco.Partita(["Allen","Jeff","Chris"])
---------- Le carte sono state distribuite
La mano di Allen contiene queste carte:
Re di Cuori
Jack di Fiori
Regina di Picche
Re di Picche
10 di Quadri
La mano di Jeff contiene queste carte:
Regina di Cuori
Jack di Picche
Jack di Cuori
Re di Quadri
Regina di Quadri
La mano di Chris contiene queste carte:
Jack di Quadri
Re di Fiori
10 di Picche
10 di Cuori
10 di Fiori
Mano di Jeff: Regina di Cuori elimina Regina di Quadri
Mano di Chris: 10 di Picche elimina 10 di Fiori
---------- Coppie scartate, inizia la partita
La mano di Allen contiene queste carte:
Re di Cuori
Jack di Fiori
Regina di Picche
Re di Picche
10 di Quadri
La mano di Jeff contiene queste carte:
Jack di Picche
Jack di Cuori
Re di Quadri
La mano di Chris contiene queste carte:
Jack di Quadri
Re di Fiori
10 di Cuori
Mano di Allen: scelta Re di Quadri
Mano di Allen: Re di Cuori elimina Re di Quadri
Mano di Jeff: scelta 10 di Cuori
Mano di Chris: scelta Jack di Fiori
Mano di Allen: scelta Jack di Cuori
Mano di Jeff: scelta Jack di Quadri
Mano di Chris: scelta Regina di Picche
Mano di Allen: scelta Jack di Quadri
Mano di Allen: Jack di Cuori elimina Jack di Quadri
Mano di Jeff: scelta Re di Fiori
Mano di Chris: scelta Re di Picche
Mano di Allen: scelta 10 di Cuori
Mano di Allen: 10 di Quadri elimina 10 di Cuori
Mano di Jeff: scelta Regina di Picche
Mano di Chris: scelta Jack di Picche
Mano di Chris: Jack di Fiori elimina Jack di Picche
Mano di Jeff: scelta Re di Picche
Mano di Jeff: Re di Fiori elimina Re di Picche
---------- La partita e' finita
La mano di Allen e' vuota
La mano di Jack contiene queste carte:
Regina di Picche
La mano di Chris e' vuota