Siguiente Subir Anterior Hola Índice

Capítulo 15

Comentarios
 

Conjuntos de objetos

15.1 Composición

Hasta ahora, hemos visto diversos ejemplos de composición. En uno de los primeros ejemplos se usaba un método de invocación que formaba parte de una expresión. Otro ejemplo es la estructura anidada de sentencias; puedes poner una sentencia if dentro de un bucle while y con otra sentencia if, etcétera.

Una vez visto este modelo y después de saber algo más acerca de listas y objetos, no debe sorprenderte descubrir que eres capaz de crear listas de objetos. Además puedes crear objetos que contengan listas (como atributos); puedes crear listas que contengan listas; puedes crear objetos que contengan objetos; etcétera.

En este capítulo y en el próximo veremos una serie de ejemplos sobre estas combinaciones, al usar objetos Carta como ejemplo. Comentarios
 

15.2 Objetos Carta

Si no estás familiarizado con los típicos juegos de cartas, ahora es el momento perfecto para conseguir una baraja, de lo contrario puede ser que este capítulo no tenga mucho sentido. Hay cincuenta y dos cartas en una baraja, donde cada una pertenece a uno de los cuatro palos y a uno de los trece rangos. Los palos son Picas, Corazones, Diamantes, y Tréboles (en orden descendente en la baraja). Los rangos son el As, 2, 3, 4, 5, 6, 7, 8, 9, 10, la Jota, la Reina, y el Rey. Dependiendo del juego, el rango del As puede ser mayor que el del Rey o menor que el del 2.

Si quisiéramos definir un nuevo objeto para representar una carta del juego, es obvio cuáles deberían ser los atributos: rango y palo. Lo que no es tan obvio es qué tipo de atributos deberían ser. Una posibilidad es usar cadenas que contengan palabras como "Picas" para palos y "Reina" para rangos. Un problema que existe con esta implementación es la dificultad de comparar cartas para ver cuál tiene un rango o un palo superior.

Una alternativa es usar números enteros para codificar los rangos y los palos. Al decir "codificar" no nos referimos a aquello que mucha gente piensa, es decir, encriptar o cifrar a un código secreto. Un informático se refiere con "codificar" a "definir un esquema entre una secuencia de números y los términos que quiere representar". Por ejemplo:

 

Picas -> 3
Corazones -> 2
Diamantes -> 1
Tréboles -> 0

Una característica obvia de este esquema es que los palos se corresponden con números enteros en orden, por lo tanto podemos comparar los palos comparando los números enteros. El esquema para los rangos es obvio; cada uno de los rangos numéricos se corresponde con un número entero, y para las figuras:

 

Jota -> 11
Reina -> 12
Rey -> 13
 

La razón por la que se usa la notación matemática para estos esquemas es que no forman parte del programa Python. Son parte del diseño del programa, pero nunca aparecen explícitamente en el código. La definición de clase para el tipo de Carta es la siguiente:

class Carta:
  def __init__(self, palo=0, rango=0):
    self.palo = palo
    self.rango = rango
 

Normalmente, proporcionamos un método de inicialización que toma un parámetro opcional para cada atributo.

Para crear un objeto que represente el 3 de Tréboles, usa esta orden:

tresDeTreboles = Carta(0, 3)
 

El primer parámetro, 0, representa el palo de Tréboles. Comentarios
 

15.3 Atributos de clase y el método __str__

Para mostrar en pantalla objetos Carta de manera que la gente pueda leerlos fácilmente, debemos definir un esquema entre los códigos de números enteros y las palabras que quieren representar. Una manera fácil de hacerlo es mediante listas de cadenas. Asignamos estas listas a los atributos de clase al principio de la definición de la clase:

class Carta:
  paloLista = ["Tréboles", "Diamantes", "Corazones", "Picas"]
  rangoLista = ["comodín", "As", "2", "3", "4", "5", "6", "7",
              "8", "9", "10", "Jota", "Reina", "Rey"]

  #método de inicialización omitido

  def __str__(self):
    return (self.rangoLista[self.rango] + " of " +
            self.paloLista[self.palo])
 

Se define un atributo de clase independientemente de un método, y se puede acceder a él desde cualquiera de los métodos de la clase.

En __str__,se usa paloLista y rangoLista para elaborar un esquema de los valores numéricos de palo y rango en cadenas. Por ejemplo, la expresión self.paloLista[self.palo] significa "usar el atributo palo del objeto self como un índice en el atributo de la clase paloLista, y seleccionar la cadena apropiada."

La razón por la que el "comodín" es el primer elemento en rangoLista es para ocupar el sitio cero de la lista, que nunca se usará. Los únicos rangos válidos son del 1 al 13. Este elemento no es del todo necesario. Lo normal habría sido empezar en el 0, pero es mas fácil codificar el 2 como el 2, el 3 como el 3, y así sucesivamente.

Con los métodos que tenemos hasta el momento, podemos crear e imprimir las cartas:

>>> carta1 = Carta(1, 11)
>>> print carta1
Jota de Diamantes
 

Los atributos de clase como paloLista son compartidos por todos los objetos Carta. La ventaja es que se puede usar cualquier objeto Carta para acceder a los atributos de clase:

>>> carta2 = Carta(1, 3)
>>> print carta2
Jota de Diamantes
 

La desventaja es que si se modifica un atributo de clase, afecta a cada instancia de la clase. Por ejemplo, si queremos cambiar el nombre de la "Jota de Diamantes" por "Jota de Ositos de Gominola", debemos hacer lo siguiente:

>>> carta1.paloLista[1] = "Ositos de Gominola"
>>> print carta1
Jota de Ositos de Gominola
 

El problema es que todos los Diamantes se convierten en ositos de gominola:

>>> print carta2
3 de ositos de gominola
 

Normalmente no es una buena idea modificar los atributos de la clase. Comentarios
 

15.4 Comparando cartas

Para los tipos primitivos, hay operadores condicionales (<, >, ==, etc.) que comparan los valores y determinan cuándo uno es mayor, menor o igual que otro. Para los tipos definidos por el usuario, podemos anular el funcionamiento del operador incorporado proporcionando un método llamado __cmp__. Por norma, __cmp__ toma dos parámetros, self y otro, y devuelve 1 si el primer objeto es mayor, -1 si el segundo objeto es mayor, y 0 si son iguales que los otros.

Algunos tipos están completamente ordenados, lo que significa que se puede comparar cualquiera de los dos elementos y decir cuál es más grande. Por ejemplo, los números enteros y los decimales están completamente ordenados. Algunos conjuntos están desordenados, lo que significa que no hay forma de decir que un elemento es mayor que otro. Por ejemplo, las frutas están desordenadas, y ésto es porque no se puede comparar manzanas y naranjas.

El conjunto del juego de cartas está parcialmente ordenado, lo que significa que a veces puedes comparar cartas y a veces no. Por ejemplo, se sabe que el 3 de Tréboles es más alto que el 2 de Tréboles, y que el 3 de Diamantes es más alto que el 3 de Tréboles. Pero cuál es mejor, ¿el 3 de Tréboles o el 2 de Diamantes? Uno tiene un rango más alto, pero el otro tiene un palo más alto.

Para comparar las cartas, tienes que decidir qué es más importante, el rango o el palo. Para ser sinceros, la elección es arbitraria. Para elegir bien, diríamos que el palo es más importante, porque una baraja de cartas se clasifica con todos los Tréboles juntos, seguidos de todos los Diamantes, etcétera.

Una vez decidido, podemos escribir __cmp__:

def __cmp__(self, otro):
  # revisa los palos
  if self.palo > otro.palo: return 1
  if self.palo < otro.palo: return -1
  # los palos son iguales... revisa los rangos
  if self.rango > otro.rango: return 1
  if self.palo < otro.palo: return -1
  # los rangos son iguales... es un vinculo
  return 0
 

En este orden, los Ases aparecen por debajo de los Doses.

Como ejercicio, modifica __cmp__ para que el As tenga un rango mayor que el Rey.

Comentarios
 

15.5 Barajas

Ahora que tenemos objetos para representar Cartas, el próximo paso es definir una clase que represente una Baraja. Obviamente, una baraja está formada por cartas, así que cada objeto Baraja contendrá una lista de cartas como atributo.

A continuación aparece la definición de la clase Baraja. El método de inicialización crea el atributo cartas y genera un conjunto estándar de cincuenta y dos cartas:

class Baraja:
  def __init__(self):
    self.cartas = []
    for palo in range(4):
      for rango in range(1, 14):
        self.cartas.append(Carta(palo, rango))
 

La manera más fácil de poblar una baraja es con un bucle anidado. El bucle externo enumera los palos del 0 al 3. El bucle interno enumera los rangos del 1 al 13. Dado que el bucle externo se ejecuta cuatro veces, y el interno trece veces, el número total de veces en las que el cuerpo se ejecuta es de cincuenta y dos veces (trece veces cuatro). Cada iteración crea una nueva instancia de Carta con el palo y el rango actual, y añade esa carta a la lista de cartas.

El método append trabaja en listas pero no lo hace en tuplas. Comentarios
 

15.6 Mostrar en pantalla la baraja

Normalmente, cuando definimos un nuevo tipo de objeto necesitamos un método que muestre sus contenidos. Para imprimir una Baraja, realizamos un recorrido por la lista enlazada e imprimimos cada Carta:

class Baraja:
  ...
  def imprimirBaraja(self):
    for carta in self.cartas:
      print carta
 

De ahora en adelante, los puntos suspensivos (...) indican que hemos omitido los demás métodos de la clase.

Como alternativa a imprimirBaraja, podemos escribir un método __str__ para la clase Baraja. La ventaja de __str__ es su gran flexibilidad. En vez de imprimir sólo los contenidos del objeto, genera una representación de tipo cadena que otros fragmentos del programa pueden manipular antes de la impresión, o guardar para su uso posterior.

Aquí se muestra una versión de __str__ que devuelve la representación en cadena de una Baraja. Para darle un poco de dinamismo, ordena las cartas en una cascada en la que cada carta está separada de la siguiente por un espacio más que de la anterior:

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

Este ejemplo muestra varias herramientas. En primer lugar, en vez de recorrer self.cartas y asignar una variable a cada carta, se utiliza i como variable de bucle y un índice en la lista de cartas.

En segundo lugar, utilizamos el operador de multiplicación de cadenas para separar cada carta con un espacio más que de la anterior. La expresión " "*¡ produce un número de espacios igual al valor actual de i.

En tercer lugar, en vez de utilizar la orden print para mostrar las cartas, utilizamos la función str. Usar un objeto como parámetro de str es igual que invocar para el objeto el método __str__.

Finalmente, se utiliza la variable s como un acumulador. Al principio, s es la cadena vacía. Cada vez que pase por el bucle, una cadena nueva se genera y se enlaza con el antiguo valor de s para adquirir un nuevo valor. Cuando el bucle termina, s contiene la cadena de representación completa de Baraja, que sería la siguiente:

>>> baraja = Baraja()
>>> print baraja
As de Tréboles
2 ode Tréboles
  3 de Tréboles
   4 de Tréboles
    5 de Tréboles
     6 de Tréboles
      7 de Tréboles
       8 de Tréboles
        9 de Tréboles
         10 de Tréboles
          Jota de Tréboles
           Reina de Tréboles
            Rey de Tréboles
             As de Diamantes
 

Y así sucesivamente. Aunque el resultado aparezca en 52 líneas, se trata de una larga cadena que contiene saltos de línea. Comentarios
 

15.7 Barajar las cartas

Si una baraja está perfectamente mezclada, entonces existe la misma probabilidad de que cualquier carta aparezca en cualquier parte de la baraja, y de que en cualquier lugar de la baraja encontremos cualquier carta.

Para barajar las cartas se usará la función randrange del módulo random. Con dos argumentos de números enteros, a y b, randrange elegirá un número entero aleatorio del intervalo a <= x b. Ya que el límite superior es estrictamente menor que b, si usamos la longitud de una lista como el segundo parámetro, estamos seguros de conseguir un índice válido. Por ejemplo, esta expresión selecciona el índice de una carta aleatoria en una baraja:

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

Una manera más fácil de mezclar una baraja es ir recorriendo las cartas, eligiéndolas al azar y cambiándolas de lugar por otra elegida al azar. Es posible que la carta se cambie por sí misma, lo que no deja de ser correcto. De hecho, si excluímos esta posibilidad, el orden de las cartas ya no dependería enteramente del azar:

class Baraja:
  ...
  def barajar(self):
    import random
    nCartas = len(self.cartas)
    for i in range(nCartas):
      j = random.randrange(i, nCartas)
      self.cartas[i], self.cartas[j] = self.cartas[j], self.cartas[i]
 

En vez de asumir que hay cincuenta y dos cartas en la baraja, obtenemos la longitud real de la lista y la almacenaremos en nCartas.

Para cada carta de la baraja, escogemos una carta aleatoria entre las que aún no han sido mezcladas. Entonces cambiamos la carta actual (i) por la carta seleccionada (j). Para intercambiar una carta usamos una asignación de tupla, como en la Sección 9.2:

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

Como ejercicio, vuelve a escribir esta línea de código sin usar una asignación de secuencia.

Comentarios
 

15.8 Repartir y eliminar cartas.

Otro método que sería útil para la clase Baraja es eliminarCarta, que toma una carta como si fuera un parámetro, lo elimina, y muestra verdadero (1) si la carta estaba en la baraja y falso (0) si fuese lo contrario:

class Baraja:
  ...
  def eliminarCarta(self, carta):
    if carta in self.cartas:
      self.cartas.remove(carta)
      return 1
    else:
      return 0
 

El operador in muestra verdadero si el primer operando está en segundo lugar, que deberá ser una lista o una tupla. Si el primer operando es un objeto, Python utilizará el método __cmp__ para determinar igualdad entre los términos de la lista. Dado que __ cmp __ en la clase Carta comprueba la igualdad profunda, en el método eliminarCarta también se comprueba la igualdad profunda.

Al repartir cartas, debemos robar y devolver la más alta. El método de lista pop proporciona un modo apropiado para hacerlo:

class Baraja:
  ...
  def popCarta(self):
    return self.cartas.pop()
 

En realidad, pop elimina la última carta de la lista, así que, en efecto, estamos sacando cartas del final de la baraja.

Una operación más es la función booleana estáVacío, que muestra verdadero si la baraja no contiene más cartas:

class Baraja:
  ...
  def estáVacío(self):
    return (len(self.cartas) == 0)
 

Comentarios
 

15.9 Glosario

codificar
Representar un conjunto de valores usando cualquier otro conjunto de valores construyendo un esquema entre ellos.
atributo de clase
Una variable que se define dentro de una definición de clase pero fuera de cualquier método. Los atributos de clase son accesibles desde cualquier método de la clase y son compartidos por todos los ejemplos de la clase.
acumulador
Una variable que se utiliza en un bucle para acumular una serie de valores, al enlazarlos en una cadena o realizar una suma acumuladora.


Siguiente Arriba Anterior Hola Índice