Next Up Previous Hi Index

Chapter 15

Insiemi di oggetti

15.1 Composizione

Uno dei primi esempi di composizione che hai visto è stato l'uso di un'invocazione di un metodo all'interno di un'espressione. Un altro esempio è stata la struttura di istruzioni annidate, con un if all'interno di un ciclo while all'interno di un altro if e così via.

Dopo aver visto questo modo di operare e aver analizzato le liste e gli oggetti, non dovresti essere sorpreso del fatto che puoi anche creare liste di oggetti. Non solo: puoi creare oggetti che contengono liste come attributi, o liste che contengono liste, oggetti che contengono oggetti e così via.

In questo capitolo e nel prossimo vedremo alcuni esempi di queste combinazioni usando l'oggetto Carta.

15.2 Oggetto Carta

Se non hai dimestichezza con le comuni carte da gioco adesso è il momento di prendere in mano un mazzo di carte, altrimenti questo capitolo non avrà molto senso. Per i nostri scopi considereremo un mazzo di carte americano: questo mazzo è composto da 52 carte, ognuna delle quali appartiene a un seme (picche, cuori, quadri, fiori, nell'ordine di importanza nel gioco del bridge) ed è identificata da un numero da 1 a 13 (detto "rango"). I valori rappresentano, in ordine crescente, l'Asso, la serie numerica da 2 a 10, il Jack, la Regina ed il Re. A seconda del gioco a cui stai giocando il valore dell'Asso può essere considerato inferiore al 2 o superiore al Re.

Volendo definire un nuovo oggetto per rappresentare una carta da gioco è ovvio che gli attributi devono essere il rango ed il seme. Non è invece evidente di che tipo debbano essere gli attributi. Una possibilità è quella di usare stringhe contenenti il seme ("Cuori") e il rango ("Regina") solo che in questo modo non c'è un sistema semplice per vedere quale carta ha il rango o il seme più elevato.

Un'alternativa è quella di usare gli interi per codificare il rango e il seme. Con "codifica" non intendiamo crittografie o traduzioni in codice segreto ma semplicemente la definizione che lega una sequenza di numeri agli oggetti che essi vogliono rappresentare. Per esempio:

Picche -> 3
Cuori -> 2
Quadri -> 1
Fiori -> 0

Un utile effetto pratico di questa mappatura è il fatto che possiamo confrontare i semi tra di loro determinando subito quale vale di più. La mappatura per il rango è abbastanza ovvia: per le carte numeriche il rango è il numero della carta mentre per le carte figurate usiamo queste associazioni:

Asso -> 1
Jack -> 11
Regina -> 12
Re -> 13

Cominciamo con il primo abbozzo di definizione di Carta e come sempre forniamo anche un metodo di inizializzazione dei suoi attributi:

class Carta:
  def __init__(self, Seme=0, Rango=0):
    self.Seme = Seme
    self.Rango = Rango

Per creare un oggetto che rappresenta il 3 di fiori useremo:

TreDiFiori = Carta(0, 3)

dove il primo argomento (0) rappresenta il seme fiori ed il secondo (3) il rango della carta.

15.3 Attributi della classe e metodo __str__

Per stampare oggetti di tipo Carta in un modo facilmente comprensibile possiamo mappare i codici interi con stringhe. Assegniamo pertanto due liste di stringhe all'inizio della definizione della classe:

class Carta:

  ListaSemi = ["Fiori", "Quadri", "Cuori", "Picche"]
  ListaRanghi = ["impossibile", "Asso", "2", "3", "4", "5", "6",\
                 "7", "8", "9", "10", "Jack", "Regina", "Re"]

  def __init__(self, Seme=0, Rango=0):
    self.Seme = Seme
    self.Rango = Rango

  def __str__(self):
    return (self.ListaRanghi[self.Rango] + " di " +
            self.ListaSemi[self.Seme])

Le due liste sono in questo caso degli attributi di classe che sono definiti all'esterno dei metodi della classe e possono essere utilizzati da qualsiasi metodo della classe.

All'interno di __str__ possiamo allora usare ListaSemi e ListaRanghi per far corrispondere i valori numerici di Seme e Rango a delle stringhe. Per fare un esempio l'espressione self.ListaSemi[self.Seme] significa "usa l'attributo Seme dell'oggetto self come indice nell'attributo di classe chiamato ListaSemi e restituisci la stringa appropriata".

Il motivo della presenza dell'elemento "impossibile" nel primo elemento di ListaRanghi è di agire come segnaposto per l'elemento 0 che non dovrebbe mai essere usato dato che il rango ha valori da 1 a 13. Meglio sprecare un elemento della lista piuttosto che dover scalare tutti i ranghi di una posizione e dover far corrispondere l'asso allo 0, il due all'1, il tre al 2, eccetera, con il rischio di sbagliare.

Con i metodi che abbiamo scritto finora possiamo già creare e stampare le carte:

>>> Carta1 = Carta(1, 11)
>>> print Carta1
Jack di Quadri

Gli attributi di classe come ListaSemi sono condivisi da tutti gli oggetti Carta. Il vantaggio è che possiamo usare qualsiasi oggetto Carta per accedere agli attributi di classe:

>>> Carta2 = Carta(1, 3)
>>> print Carta2
3 di Quadri
>>> print Carta2.ListaSemi[1]
Quadri

Lo svantaggio sta nel fatto che se modifichiamo un attributo di classe questo cambiamento si riflette in ogni istanza della classe. Per esempio se decidessimo di cambiare il seme "Quadri" in "Bastoni"...

>>> Carta1.ListaSemi[1] = "Bastoni"
>>> print Carta1
Jack di Bastoni

...tutti i Quadri diventerebbero dei Bastoni:

>>> print Carta2
3 di Bastoni

Non è solitamente una buona idea modificare gli attributi di classe.

15.4 Confronto tra carte

Per i tipi primitivi sono già definiti operatori condizionali (<, >, ==, ecc.) che confrontano i valori e determinano se un operatore è più grande, più piccolo o uguale ad un altro. \ Per i tipi definiti dall'utente possiamo ridefinire il comportamento di questi operatori aggiungendo il metodo __cmp__. Per convenzione __cmp__ prende due parametri, self e Altro, e ritorna 1 se il primo è il più grande, -1 se è più grande il secondo e 0 se sono uguali.

Alcuni tipi sono completamente ordinati, il che significa che puoi confrontare due elementi qualsiasi e determinare sempre quale sia il più grande tra di loro. Per esempio i numeri interi e quelli in virgola mobile sono completamente ordinati. Altri tipi sono disordinati, nel senso che non esiste un modo logico per stabilire quale sia il più grande, così come non è possibile stabilire tra una serie di colori quale sia il "minore".

L'insieme delle carte da gioco è parzialmente ordinato e ciò significa che qualche volta puoi confrontare due carte e qualche volta no. Per fare un esempio sai che il 3 di Fiori è più alto del 2 di Fiori e il 3 di Quadri più alto del 3 di Fiori. Fino a questo punto il loro valore relativo e il conseguente ordine sono chiari. Ma qual è la carta più alta se dobbiamo scegliere tra 3 di Fiori e 2 di Quadri? Una ha il rango più alto, l'altra il seme.

Per rendere confrontabili le carte dobbiamo innanzitutto decidere quale attributo sia il più importante, se il rango o il seme. La scelta è arbitraria e per il nostro studio decideremo che il seme ha priorità rispetto al rango.

Detto questo possiamo scrivere __cmp__:

def __cmp__(self, Altro):

  # controlla il seme
  if self.Seme > Altro.Seme: return 1
  if self.Seme < Altro.Seme: return -1

  # se i semi sono uguali controlla il rango
  if self.Rango > Altro.Rango: return 1
  if self.Rango < Altro.Rango: return -1

  # se anche i ranghi sono uguali le carte sono uguali!
  return 0

In questo tipo di ordinamento gli Assi hanno valore più basso dei 2.

Esercizio: modifica __cmp__ così da rendere gli Assi più importanti dei Re.

15.5 Mazzi

Ora che abbiamo oggetti per rappresentare le carte il passo più logico è quello di definire una classe per rappresentare il Mazzo. Il mazzo è composto di carte così ogni oggetto Mazzo conterrà una lista di carte come attributo.

Quella che segue è la definizione di classe della classe Mazzo. Il metodo di inizializzazione crea l'attributo Carte e genera le 52 carte standard:

class Mazzo:
  def __init__(self):
    self.Carte = []
    for Seme in range(4):
      for Rango in range(1, 14):
        self.Carte.append(Carta(Seme, Rango))

Il modo più semplice per creare un mazzo è per mezzo di un ciclo annidato: il ciclo esterno numera i semi da 0 a 3, quello interno i ranghi da 1 a 13. Dato che il ciclo esterno viene eseguito 4 volte e quello interno 13 il corpo è eseguito un totale di 52 volte (4 per 13). Ogni iterazione crea una nuova istanza di Carta con seme e rango correnti ed aggiunge la carta alla lista Carte.

Il metodo append lavora sulle liste ma non sulle tuple (che sono immutabili).

15.6 Stampa del mazzo

Com'è consueto dopo aver creato un nuovo tipo di oggetto è utile scrivere un metodo per poterne stampare il contenuto. Per stampare Mazzo attraversiamo la lista stampando ogni elemento Carta:

class Mazzo:
  ...
  def StampaMazzo(self):
    for Carta in self.Carte:
      print Carta

Come alternativa a StampaMazzo potremmo anche riscrivere il metodo __str__ per la classe Mazzo. Il vantaggio nell'uso di __str__ sta nel fatto che è più flessibile. Piuttosto che limitarsi a stampare il contenuto di un oggetto __str__ genera infatti una rappresentazione sotto forma di stringa che altre parti del programma possono manipolare o che può essere memorizzata in attesa di essere usata in seguito.

Ecco una versione di __str__ che ritorna una rappresentazione di un Mazzo come stringa. Tanto per aggiungere qualcosa facciamo anche in modo di indentare ogni carta rispetto alla precedente:

class Mazzo:
  ...
  def __str__(self):
    s = ""
    for i in range(len(self.Carte)):
      s = s + " "*i + str(self.Carte[i]) + "\n"
    return s

Questo esempio mostra un bel po' di cose.

Prima di tutto invece di attraversare self.Carte e assegnare ogni carta ad una variabile stiamo usando i come variabile del ciclo e come indice della lista delle carte.

In secondo luogo stiamo usando l'operatore di moltiplicazione delle stringhe per indentare le carte. L'espressione " "*i infatti produce un numero di spazi pari a i.

Terzo, invece di usare un comando print per stampare le carte usiamo la funzione str. Passare un oggetto come argomento a str è equivalente ad invocare il metodo __str__ sull'oggetto.

Infine stiamo usando la variabile s come accumulatore. Inizialmente s è una stringa vuota. Ogni volta che passiamo attraverso il ciclo viene generata e concatenata a s una nuova stringa. Quando il ciclo termina s contiene la rappresentazione completa dell'oggetto Mazzo sotto forma di stringa:

>>> Mazzo1 = Mazzo()
>>> print Mazzo1
Asso di Fiori
2 di Fiori
  3 di Fiori
   4 di Fiori
    5 di Fiori
     6 di Fiori
      7 di Fiori
       8 di Fiori
        9 di Fiori
         10 di Fiori
          Jack di Fiori
           Regina di Fiori
            Re di Fiori
             Asso di Quadri
              ...

Anche se il risultato appare come una serie di 52 righe (una per ogni carta) in realtà si tratta di una singola stringa che contiene caratteri di ritorno a capo per poter essere stampata su più righe.

15.7 Mescolare il mazzo

Se un mazzo è perfettamente mescolato ogni carta ha la stessa probabilità di comparire in una posizione qualsiasi.

Per mescolare il mazzo useremo la funzione randrange del modulo random. randrange prende due argomenti interi (a e b) e sceglie un numero casuale intero nell'intervallo a <= x < b. Dato che il limite superiore è escluso possiamo usare la lunghezza di una lista come secondo parametro avendo la garanzia della validità dell'indice. Questa espressione sceglie l'indice di una carta casuale nel mazzo:

random.randrange(0, len(self.Carte))

Un modo utile per mescolare un mazzo è scambiare ogni carta con un'altra scelta a caso. È possibile che la carta possa essere scambiata con se stessa ma questa situazione è perfettamente accettabile. Infatti se escludessimo questa possibilità l'ordine delle carte sarebbe meno casuale:

class Mazzo:
  ...
  def Mescola(self):
    import random
    NumCarte = len(self.Carte)
    for i in range(NumCarte):
      j = random.randrange(i, NumCarte)
      self.Carte[i], self.Carte[j] = self.Carte[j], self.Carte[i]

Piuttosto che partire dal presupposto che le carte del mazzo siano sempre 52 abbiamo scelto di ricavare la lunghezza della lista e memorizzarla in NumCarte.

Per ogni carta del mazzo abbiamo scelto casualmente una carta tra quelle non ancora mescolate. Poi abbiamo scambiato la carta corrente (i) con la carta selezionata (j). Per scambiare le due carte abbiamo usato un'assegnazione di una tupla, come si è già visto nella sezione 9.2:

self.Carte[i], self.Carte[j] = self.Carte[j], self.Carte[i]

Esercizio: riscrivi questa riga di codice senza usare un'assegnazione di una tupla.

15.8 Rimuovere e distribuire le carte

Un altro metodo utile per la classe Mazzo è RimuoviCarta che permette di rimuovere una carta dal mazzo ritornando vero (1) se la carta era presente e falso (0) in caso contrario:

class Mazzo:
  ...
  def RimuoviCarta(self, Carta):
    if Carta in self.Carte:
      self.Carte.remove(Carta)
      return 1
    else:
      return 0

L'operatore in ritorna vero se il primo operando è contenuto nel secondo. Quest'ultimo deve essere una lista o una tupla. Se il primo operando è un oggetto, Python usa il metodo __cmp__ dell'oggetto per determinare l'uguaglianza tra gli elementi della lista. Dato che __cmp__ nella classe Carta controlla l'uguaglianza forte il metodo RimuoviCarta usa anch'esso l'uguaglianza forte.

Per distribuire le carte si deve poter rimuovere la prima carta del mazzo e il metodo delle liste pop fornisce un ottimo sistema per farlo:

class Mazzo:
  ...
  def PrimaCarta(self):
    return self.Carte.pop()

In realtà pop rimuove l'ultima carta della lista, così stiamo in effetti togliendo dal fondo del mazzo, ma dal nostro punto di vista questa anomalia è indifferente.

Una operazione che può essere utile è la funzione booleana EVuoto che ritorna vero (1) se il mazzo non contiene più carte:

class Mazzo:
  ...
  def EVuoto(self):
    return (len(self.Carte) == 0)

15.9 Glossario

Mappare
rappresentare un insieme di valori usando un altro insieme di valori e costruendo una mappa di corrispondenza tra i due insiemi.
Codificare
in campo informatico sinonimo di mappare.
Attributo di classe
variabile definita all'interno di una definizione di classe ma al di fuori di qualsiasi metodo. Gli attributi di classe sono accessibili da ognuno dei metodi della classe e sono condivisi da tutte le istanze della classe.
Accumulatore
variabile usata in un ciclo per accumulare una serie di valori, concatenati sotto forma di stringa o sommati per ottenere un valore totale.


Next Up Previous Hi Index