Previous Up Next

Buy this book at Amazon.com

Chapter 18  Ereditarietà

La caratteristica più frequentemente associata alla programmazione orientata agli oggetti è l’ereditarietà, che è la capacità di definire una nuova classe come versione modificata di una classe già esistente. In questo capitolo illustrerò l’ereditarietà usando delle classi che rappresentano carte da gioco, mazzi di carte e mani di poker.

Se non giocate a poker, potete leggere qualcosa in proposito sul sito http://it.wikipedia.org/wiki/Poker, ma non è un obbligo: vi spiegherò quello che serve.

Il codice degli esempi di questo capitolo è scaricabile da http://thinkpython2.com/code/Card.py.

18.1  Oggetti Carta

In un mazzo ci sono 52 carte, e ciascuna appartiene a uno tra quattro semi e a uno tra tredici valori. I semi sono Picche, Cuori, Quadri e Fiori (in ordine decrescente nel gioco del bridge). I valori sono Asso, 2, 3, 4, 5, 6, 7, 8, 9, 10, Fante, Regina e Re. A seconda del gioco, l’Asso può essere superiore al Re o inferiore al 2.

Se vogliamo definire un nuovo oggetto che rappresenti una carta da gioco, è evidente quali attributi dovrebbe avere: valore e seme. È meno evidente stabilire di che tipo devono essere questi attributi. Una possibilità è usare stringhe contenenti parole come 'Picche' per i semi e 'Regina' per i valori. Ma un problema di questa implementazione è che non è facile confrontare le carte per vedere quale abbia un seme o un valore superiore.

Un’alternativa è usare degli interi per codificare valori e semi. In questo contesto, “codificare” significa determinare una corrispondenza tra numeri e semi o numeri e valori. Non significa che debba essere un segreto (quello è “criptare”).

Per esempio, questa tabella mostra i semi e i corrispondenti codici interi:

Picche3
Cuori2
Quadri1
Fiori0

In questo modo, diventa facile confrontare le carte: siccome ai semi più alti corrispondono numeri più alti, si possono confrontare i semi confrontando i loro codici corrispondenti.

Nel caso dei valori, la corrispondenza è abbastanza immediata: ogni valore numerico corrisponde al rispettivo intero, mentre per le figure:

Fante11
Regina12
Re13

Uso il simbolo ↦ per chiarire che queste corrispondenze non fanno parte del programma Python. Fanno parte del progetto del programma, ma non compaiono esplicitamente nel codice.

Ecco come si può presentare la definizione di classe per Carta:

class Carta:
    """Rappresenta una carta da gioco standard."""

    def __init__(self, seme=0, valore=2):
        self.seme = seme
        self.valore = valore

Come al solito, il metodo init prevede un parametro opzionale per ciascun attributo. La carta di default è il 2 di fiori.

Per creare una carta, si chiama la classe Carta con il seme e il valore desiderati.

regina_di_quadri = Carta(1, 12)

18.2  Attributi di classe

Per stampare gli oggetti Carta in un modo comprensibile agli utenti, occorre stabilire una corrispondenza dai codici interi ai relativi semi e valori. Un modo naturale per farlo è usare delle liste di stringhe, che assegneremo a degli attributi di classe:

# all'interno della classe Carta:

    nomi_semi = ['Fiori', 'Quadri', 'Cuori', 'Picche']
    nomi_valori = [None, 'Asso', '2', '3', '4', '5', '6', '7', 
              '8', '9', '10', 'Fante', 'Regina', 'Re']

    def __str__(self):
        return '%s di %s' % (Carta.nomi_valori[self.valore],
                             Carta.nomi_semi[self.seme])

Variabili come nomi_semi e nomi_valori, che sono definite dentro la classe ma esternamente a ogni metodo, sono chiamate attributi di classe perché sono associati all’oggetto classe Carta.

Questo termine li distingue da variabili come seme e valore, che sono chiamati attributi di istanza perché sono associati ad una specifica istanza.

Ad entrambi i tipi si accede usando la notazione a punto. Per esempio in __str__, self è un oggetto carta e self.valore è il suo valore. Allo stesso modo, Carta è un oggetto classe, e Carta.nomi_valori è una lista di stringhe associata alla classe.

Ogni carta ha i suoi propri seme e valore, ma esiste una sola copia di nomi_semi e nomi_valori.

Mettendo insieme il tutto, l’espressione Carta.nomi_valori[self.valore] significa “usa l’attributo valore dell’oggetto self come indice nella lista nomi_valori dalla classe Carta, e seleziona la stringa corrispondente.”

Il primo elemento della lista nomi_valori è None perché non esiste una carta di valore zero. Includendo None come segnaposto, otteniamo una corrispondenza corretta per cui all’indice 2 corrisponde la stringa '2', e così via. Per evitare questo trucco, avremmo potuto usare un dizionario al posto di una lista.

Con i metodi che abbiamo visto fin qui, possiamo creare e stampare i nomi delle carte:

>>> carta1 = Carta(2, 11)
>>> print(carta1)
Fante di Cuori

Figure 18.1: Diagramma di oggetto.

La Figura 18.1 è un diagramma dell’oggetto classe Carta e di una Carta, sua istanza. Carta è un oggetto classe, quindi è di tipo type. carta1 invece è di tipo Carta. (Per motivi di spazio ho omesso i contenuti di nomi_semi e nomi_valori).

18.3  Confrontare le carte

Per i tipi predefiniti, esistono gli operatori relazionali (<, >, ==, etc.) che permettono di confrontare i valori e determinare quale è maggiore, minore o uguale a un altro. Per i tipi personalizzati, possiamo sovrascrivere il comportamento degli operatori predefiniti grazie a un metodo speciale chiamato __lt__, che sta per “less than”.

__lt__ richiede due parametri, self e other, e restituisce True se self è minore di other.

L’ordinamento corretto delle carte da gioco non è immediato. Per esempio, tra il 3 di Fiori e il 2 di Quadri, quale è più grande? Una carta ha un valore maggiore, ma l’altra ha un seme superiore. Per confrontare le carte, bisogna prima stabilire se è più importante il seme oppure il valore.

La risposta dipenderà dalle regole del gioco a cui stiamo giocando, ma per semplificare supponiamo che sia più importante il seme, per cui le carte di Picche sovrastano tutte quelle di Quadri, e così via.

Deciso questo, possiamo scrivere __lt__:

# all'interno della classe Carta:

    def __lt__(self, other):
        # controlla i semi
        if self.seme < other.seme: return True
        if self.seme > other.seme: return False

        # semi uguali... controlla i valori
        return self.valore < other.valore

Potete scriverlo anche in modo più compatto, usando un confronto di tuple:

# all'interno della classe Carta:

    def __lt__(self, other):
        t1 = self.seme, self.valore
        t2 = other.seme, other.valore
        return t1 < t2

Come esercizio, scrivete un metodo __lt__ per gli oggetti Tempo. Potete usare un confronto di tuple, ma anche prendere in considerazione di confrontare degli interi.

18.4  Mazzi di carte

Ora che abbiamo le carte, il prossimo passo è definire i Mazzi. Dato che un mazzo è composto di carte, è ovvio che ogni Mazzo contenga una lista di carte come attributo.

Quella che segue è una definizione di classe di Mazzo. Il metodo init crea l’attributo carte e genera l’insieme standard di 52 carte:

class Mazzo:

    def __init__(self):
        self.carte = []
        for seme in range(4):
            for valore in range(1, 14):
                carta = Carta(seme, valore)
                self.carte.append(carta)

Il modo più facile di popolare il mazzo è quello di usare un ciclo nidificato. Il ciclo più esterno enumera i semi da 0 a 3; quello interno enumera i valori da 1 a 13. Ogni iterazione crea una nuova carta del seme e valore correnti e la accoda nella lista self.carte.

18.5  Stampare il mazzo

Ecco un metodo __str__ per Mazzo:

#all'interno della classe Mazzo:

    def __str__(self):
        res = []
        for carta in self.carte:
            res.append(str(carta))
        return '\n'.join(res)

Questo metodo illustra un modo efficiente di accumulare una stringa lunga: costruire una lista di stringhe e poi usare il metodo delle stringhe join. La funzione predefinita str invoca il metodo __str__ su ciascuna carta e restituisce la rappresentazione della stringa.

Dato che invochiamo join su un carattere di ritorno a capo, le carte sono stampate su righe separate. Ed ecco quello che risulta:

>>> mazzo = Mazzo()
>>> print(mazzo)
Asso di Fiori
2 di Fiori
3 di Fiori
...
10 di Picche
Fante di Picche
Regina di Picche
Re di Picche

Anche se il risultato viene visualizzato su 52 righe, si tratta di un’unica lunga stringa che contiene caratteri di ritorno a capo.

18.6  Aggiungere, togliere, mescolare e ordinare

Per distribuire le carte, ci serve un metodo che tolga una carta dal mazzo e la restituisca. Il metodo delle liste pop è adatto allo scopo:

#all'interno della classe Mazzo:

    def togli_carta(self):
        return self.carte.pop()

Siccome pop rimuove l’ultima carta della lista, è come se distribuissimo le carte dal fondo del mazzo.

Per aggiungere una carta, usiamo il metodo delle liste append:

#all'interno della classe Mazzo:

    def aggiungi_carta(self, carta):
        self.carte.append(carta)

Un metodo come questo, che usa in realtà un altro metodo senza fare molto di più, da alcuni viene chiamato impiallacciatura. Questa metafora deriva dall’industria del legno: l’impiallaciatura consiste nell’incollare un sottile strato di legno di buona qualità sulla superficie di un pannello economico, per migliorarne l’aspetto.

In questo caso, aggiungi_carta è un metodo “sottile” che esprime un’operazione su una lista, in una forma appropriata per i mazzi di carte. Esso migliora l’aspetto, ovvero l’interfaccia, dell’implementazione.

Per fare un altro esempio, scriviamo anche un metodo per un Mazzo di nome mescola, usando la funzione shuffle contenuta nel modulo random:

# all'interno della classe Mazzo:
            
    def mescola(self):
        random.shuffle(self.carte)

Non scordate di importare random.

Come esercizio, scrivete un metodo per Mazzo di nome ordina che usi il metodo delle liste sort per ordinare le carte in un Mazzo. Per determinare il criterio di ordinamento, sort utilizza il metodo __lt__ che abbiamo definito.

18.7  Ereditarietà

L’ereditarietà è la capacità di definire una nuova classe come versione modificata di una classe già esistente.

Come esempio, supponiamo di voler creare una classe che rappresenti una “mano” di carte, vale a dire un gruppo di carte distribuite a un giocatore. Una mano è simile a un mazzo: entrambi sono fatti di carte, ed entrambi richiedono operazioni come l’aggiunta e la rimozione di carte.

D’altra parte, ci sono altre operazioni che servono per la mano ma che non hanno senso per il mazzo. Nel poker, ad esempio, dobbiamo confrontare due mani per vedere quale vince. Nel bridge, è utile calcolare il punteggio della mano per decidere la dichiarazione.

Questo tipo di relazione tra classi—simili, ma non uguali—porta all’ereditarietà.

Per definire una nuova classe che eredita da una classe esistente, basta scrivere tra parentesi il nome della classe esistente:

class Mano(Mazzo):
    """Rappresenta una mano di carte da gioco."""

Questa definizione indica che Mano eredita da Mazzo; ciò comporta che per Mano possiamo utilizzare i metodi di Mazzo come togli_carta e aggiungi_carta.

Quando un nuova classe eredita da una esistente, quest’ultima è chiamata madre (o superclasse) e quella nuova è chiamata figlia (o sottoclasse).

In questo esempio, Mano eredita __init__ da Mazzo, ma in questo caso il metodo non fa la cosa giusta: invece di popolare la mano con 52 nuove carte, il metodo init di Mano dovrebbe inizializzare carte con una lista vuota.

Ma se noi specifichiamo un nuovo metodo init nella classe Mano, esso andrà a sovrascrivere quello della classe madre Mazzo:

# all'interno della classe Mano:

    def __init__(self, label=''):
        self.carte = []
        self.label = label

Allora, quando si crea una Mano, Python invoca questo metodo init specifico e non quello di Mazzo:

>>> mano = Mano('nuova mano')
>>> mano.carte
[]
>>> mano.label
'nuova mano'

Gli altri metodi vengono ereditati da Mazzo, pertanto possiamo usare togli_carta e aggiungi_carta per distribuire una carta:

>>> mazzo = Mazzo()
>>> carta = mazzo.togli_carta()
>>> mano.aggiungi_carta(carta)
>>> print(mano)
Re di Picche

Viene poi spontaneo incapsulare questo codice in un metodo di nome sposta_carte:

# all'interno della classe Mazzo:

    def sposta_carte(self, mano, num):
        for i in range(num):
            mano.aggiungi_carta(self.togli_carta())

sposta_carte prende come argomenti un oggetto Mano e il numero di carte da distribuire. Modifica sia self che mano, e restituisce None.

In alcuni giochi, le carte si spostano da una mano all’altra, o da una mano di nuovo al mazzo. Potete usare sposta_carte per qualsiasi di queste operazioni: self può essere sia un Mazzo che una Mano, e mano, a dispetto del nome, può anche essere un Mazzo.

L’ereditarietà è una caratteristica utile. Certi programmi che sarebbero ripetitivi senza ereditarietà, possono invece essere scritti in modo più elegante. Facilita il riuso del codice, poiché potete personalizzare il comportamento delle superclassi senza doverle modificare. In certi casi, la struttura dell’ereditarietà rispecchia quella del problema, il che rende il programma più facile da capire.

D’altra parte, l’ereditarietà può rendere il programma difficile da leggere. Quando viene invocato un metodo, a volte non è chiaro dove trovare la sua definizione. Il codice rilevante può essere sparso tra moduli diversi. Inoltre, molte cose che possono essere fatte usando l’ereditarietà si possono fare anche, o talvolta pure meglio, senza di essa.

18.8  Diagrammi di classe

Sinora abbiamo visto i diagrammi di stack, che illustrano lo stato del programma, e i diagrammi di oggetto, che mostrano gli attributi di un oggetto e i loro valori. Questi diagrammi rappresentano una istantanea nell’esecuzione del programma, e quindi cambiano nel corso del programma.

Sono anche molto dettagliati, per alcuni scopi anche troppo. Un diagramma di classe è una rappresentazione più astratta della struttura di un programma. Invece di mostrare singoli oggetti, mostra le classi e le relazioni che sussistono tra le classi.

Ci sono alcuni tipi diversi di relazioni tra classi:

  • Oggetti in una classe possono contenere riferimenti a oggetti in un’altra classe. Per esempio, ogni Rettangolo contiene un riferimento a un Punto, e ogni Mazzo contiene riferimenti a molte Carte. Questo tipo di relazione è chiamata HAS-A (ha-un), come in: “un Rettangolo ha un Punto”.
  • Una classe può ereditare da un’altra. Questa relazione è detta IS-A (è-un), come in: “una Mano è un tipo di Mazzo”.
  • Una classe può dipendere da un altra, nel senso che oggetti di una classe possono prendere come parametri oggetti di una seconda classe oppure usarli per svolgere parte delle elaborazioni. Una relazione di questo tipo è detta dipendenza.

Un diagramma di classe è una rappresentazione grafica di queste relazioni. Per esempio, la Figura 18.2 mostra le relazioni tra Carta, Mazzo e Mano.


Figure 18.2: Diagramma di classe.

La freccia con un triangolo vuoto rappresenta la relazione IS-A: in questo caso indica che Mano eredita da Mazzo.

La freccia standard rappresenta la relazione HAS-A; in questo caso un Mazzo ha riferimenti agli oggetti Carta.

L’asterisco (*) vicino alla testa della freccia indica una molteplicità, cioè quante Carte ha un Mazzo. Una molteplicità può essere un numero semplice, come 52, un intervallo come 5..7, o un asterisco che indica che un Mazzo può contenere un numero qualsiasi di Carte.

In questo diagramma non vi sono dipendenze. In genere, verrebbero illustrate con delle frecce tratteggiate. Se vi sono parecchie dipendenze, talvolta vengono omesse.

Un diagramma più dettagliato dovrebbe evidenziare che un Mazzo contiene in realtà una lista di Carte, ma i tipi predefiniti come liste e dizionari di solito non vengono inclusi in questi diagrammi.

18.9  Debug

L’ereditarietà può rendere il debug difficoltoso, perché quando invocate un metodo su un oggetto, può risultare laborioso capire esattamente quale sia il metodo che viene invocato.

Supponiamo che stiate scrivendo una funzione che lavori su oggetti Mano. Vorreste che fosse valida per Mani di tutti i tipi come ManiDiPoker, ManiDiBridge ecc. Se invocate un metodo come mescola, potrebbe essere quello definito in Mazzo, ma se qualcuna delle sottoclassi sovrascrive il metodo, avrete invece quella diversa versione. Questo comportamento è appropriato, ma a volte può confondere.

Quando siete incerti sul flusso di esecuzione del vostro programma, la soluzione più semplice è aggiungere istruzioni di stampa all’inizio di ogni metodo importante. Se Mazzo.mescola stampa un messaggio come Sto eseguendo Mazzo.mescola, allora il programma traccia il flusso di esecuzione mentre viene eseguito.

In alternativa, potete usare la funzione seguente, che richiede un oggetto e un nome di metodo (come stringa) e restituisce la classe che contiene la definizione del metodo:

def trova_classe_def(obj, nome_metodo):
    for ty in type(obj).mro():
        if nome_metodo in ty.__dict__:
            return ty

Ecco un esempio:

>>> mano = Mano()
>>> trova_classe_def(mano, 'mescola')
<class 'Carta.Mazzo'>

Quindi il metodo mescola di questa Mano è quello definito inMazzo.

trova_classe_def usa il metodo mro per ricavare la lista degli oggetti classe (tipi) in cui verrà effettuata la ricerca dei metodi. “MRO” sta per Method Resolution Order (ordine di risoluzione dei metodi), che è la sequenza di classi che Python ricerca per “risolvere” un nome di metodo.

Un consiglio per la progettazione di un programma: quando sovrascrivete un metodo, l’interfaccia del nuovo metodo dovrebbe essere la stessa di quello sostituito: deve richiedere gli stessi parametri, restituire lo stesso tipo, rispettare le stesse precondizioni e postcondizioni. Se rispettate questa regola, vedrete che ogni funzione progettata per un’istanza di una superclasse, come Mazzo, funzionerà anche con le istanze delle sottoclassi come Mano e ManoDiPoker.

Se violate questa regola, conosciuta come “principio di sostituzione di Liskov”, il vostro codice crollerà come (perdonatemi) un castello di carte.

18.10  Incapsulamento dei dati

Il capitolo precedente ha illustrato una tecnica di sviluppo detta “progettazione orientata agli oggetti”. Abbiamo identificato gli oggetti che ci servivano—come Tempo, Punto e Rettangolo—e definito le classi per rappresentarli. Per ciascuno c’è un’evidente corrispondenza tra l’oggetto e una qualche entità del mondo reale (o per lo meno del mondo della matematica).

Ma altre volte la scelta degli oggetti e del modo in cui interagiscono è meno ovvia. In questo caso serve una tecnica di sviluppo diversa. Nella stessa maniera in cui abbiamo scoperto le interfacce delle funzioni per mezzo dell’incapsulamento e della generalizzazione, scopriamo ora le interfacce delle classi tramite l’incapsulamento dei dati.

L’analisi di Markov, vista nel Paragrafo 13.8, è un buon esempio. Se scaricate il mio codice dal sito http://thinkpython2.com/code/markov.py, vi accorgerete che usa due variabili globali—suffix_map e prefix—che vengono lette e scritte da più funzioni.

suffix_map = {}        
prefix = ()            

Siccome queste variabili sono globali, possiamo eseguire una sola analisi alla volta. Se leggessimo due testi contemporaneamente, i loro prefissi e suffissi verrebbero aggiunti nella stessa struttura di dati (il che produce comunque alcuni interessanti testi generati).

Per eseguire analisi multiple mantenendole separate, possiamo incapsulare lo stato di ciascuna analisi in un oggetto. Ecco come si presenta:

class Markov:

    def __init__(self):
        self.suffix_map = {}
        self.prefix = ()    

Poi, trasformiamo le funzioni in metodi. Ecco per esempio elabora_parola:

    def elabora_parola(self, parola, ordine=2):
        if len(self.prefix) < ordine:
            self.prefix += (parola,)
            return

        try:
            self.suffix_map[self.prefix].append(parola)
        except KeyError:
            # se non c'e' una voce per questo prefisso, creane una
            self.suffix_map[self.prefix] = [parola]

        self.prefix = shift(self.prefix, parola)        

Questa trasformazione di un programma—cambiarne la forma senza cambiarne il comportamento—è un altro esempio di refactoring (vedi Paragrafo 4.7).

L’esempio suggerisce una tecnica di sviluppo per progettare oggetti e metodi:

  1. Cominciare scrivendo funzioni che leggono e scrivono variabili globali (dove necessario)
  2. Una volta ottenuto un programma funzionante, cercare le associazioni tra le variabili globali e le funzioni che le usano.
  3. Incapsulare le variabili correlate come attributi di un oggetto.
  4. Trasformare le funzioni associate in metodi della nuova classe.

Come esercizio, scaricate il mio codice da (http://thinkpython2.com/code/markov.py), e seguite i passi appena descritti per incapsulare le varibili globali come attributi di una nuova classe chiamata Markov. Soluzione: http://thinkpython2.com/code/Markov.py (notare la M maiuscola).

18.11  Glossario

codificare:
Rappresentare un insieme di valori usando un altro insieme di valori e costruendo una mappatura tra di essi.
attributo di classe:
Attributo associato ad un oggetto classe. Gli attributi di classe sono definiti all’interno di una definizione di classe ma esternamente ad ogni metodo.
attributo di istanza:
Attributo associato ad un’istanza di una classe.
impiallacciatura:
Metodo o funzione che fornisce un’interfaccia diversa a un’altra funzione, senza effettuare ulteriori calcoli.
ereditarietà:
Capacità di definire una classe come versione modificata di una classe già definita in precedenza.
classe madre o superclasse:
Classe dalla quale una classe figlia eredita.
classe figlia o sottoclasse:
Nuova classe creata ereditando da una classe esistente.
relazione IS-A:
Relazione tra una classe figlia e la sua classe madre.
relazione HAS-A:
Relazione tra due classi dove le istanze di una classe contengono riferimenti alle istanze dell’altra classe.
dipendenza:
Relazione tra due classi dove istanze di una classe utilizzano istanze dell’altra classe, ma senza conservarle sotto forma di attributi.
diagramma di classe:
Diagramma che illustra le classi di un programma e le relazioni tra di esse.
molteplicità:
Notazione in un diagramma di classe che mostra, per una relazione HAS-A, quanti riferimenti ad istanze di un’altra classe ci sono.
incapsulamento dei dati:
Tecnica di sviluppo che prevede un prototipo che usa variabili globali e una versione finale in cui le variabili globali vengono trasformate in attributi di istanza.

18.12  Esercizi

Esercizio 1   Dato il seguente programma, disegnate un diagramma di classe UML (Unified Modeling Language) che illustri queste classi e le relazioni che intercorrono tra esse.
class PingPongMadre:
    pass

class Ping(PingPongMadre):
    def __init__(self, pong):
        self.pong = pong


class Pong(PingPongMadre):
    def __init__(self, pings=None):
        if pings is None:
            self.pings = []
        else:
            self.pings = pings

    def add_ping(self, ping):
        self.pings.append(ping)

pong = Pong()
ping = Ping(pong)
pong.add_ping(ping)


Esercizio 2  

Scrivete un metodo per Mazzo di nome dai_mani che prenda come parametri il numero di mani e il numero di carte da dare a ciascuna mano, e crei il numero stabilito di oggetti Mano, distribuisca il numero prefissato di carte a ogni mano e restituisca una lista delle Mani.


Esercizio 3  

Quelle che seguono sono le possibili combinazioni nel gioco del poker, in ordine crescente di valore e decrescente di probabilità:

coppia:
due carte dello stesso valore
doppia coppia:
due coppie di carte dello stesso valore
tris:
tre carte dello stesso valore
scala:
cinque carte con valori in sequenza (gli assi possono essere sia la carta di valore inferiore che quella di valore superiore, per cui Asso-2-3-4-5 è una scala, e anche 10-Fante-Regina-Re-Asso, ma non Regina-Re-Asso-2-3).
colore:
cinque carte dello stesso seme
full:
tre carte dello stesso valore più una coppia di carte dello stesso valore
poker:
quattro carte dello stesso valore
scala reale:
cinque carte dello stesso seme in scala (definita come sopra)

Scopo di questo esercizio è stimare la probabilità di avere servita una di queste combinazioni.

  1. Scaricate i file seguenti da http://thinkpython2.com/code:
    Card.py
    : Versione completa delle classi Carta, Mazzo e Mano di questo capitolo.
    PokerHand.py
    : Implementazione incompleta di una classe che rappresenta una mano di poker con del codice di prova.
  2. Se eseguite PokerHand.py, serve delle mani di sette carte e controlla se qualcuna contenga un colore. Leggete attentamente il codice prima di proseguire.
  3. Aggiungete dei metodi a PokerHand.py di nome ha_coppia, ha_doppiacoppia, ecc. che restituiscano True o False a seconda che le mani soddisfino o meno il rispettivo criterio. Il codice deve funzionare indipendentemente dal numero di carte che contiene la mano (5 e 7 carte sono i casi più comuni).
  4. Scrivete un metodo di nome classifica che riconosca la combinazione più elevata in una mano e imposta di conseguenza l’attributo label. Per esempio, una mano di 7 carte può contenere un colore e una coppia; deve essere etichettata “colore”.
  5. Quando siete sicuri che i vostri metodi di classificazione funzionano, il passo successivo è stimare la probabilità delle varie mani. Scrivete una funzione in PokerHand.py che mescoli un mazzo di carte, lo divida in mani, le classifichi e conti quante volte compare ciascuna combinazione.
  6. Stampate una tabella delle combinazioni con le rispettive probabilità. Eseguite il vostro programma con numeri sempre più grandi di mani finché i valori ottenuti convergono ad un ragionevole grado di accuratezza. Confrontate i vostri risultati con i valori pubblicati su http://en.wikipedia.org/wiki/Hand_rankings.

Soluzione: http://thinkpython2.com/code/PokerHandSoln.py.

Buy this book at Amazon.com

Contribute

If you would like to make a contribution to support my books, you can use the button below. Thank you!
Pay what you want:

Are you using one of our books in a class?

We'd like to know about it. Please consider filling out this short survey.



Previous Up Next