Cesar Systems

Herramientas Informaticas

15.4. Comparación de naipes

Para los tipos primitivos, existen operadores condicionales (, ==, etc.) que comparan valores y determinan cuando uno es mayor, menor, o igual a otro.

Para los tipos definidos por el usuario, podemos sustituir el comportamiento de los operadores internos si proporcionamos un metodo llamado __cmp__ . Por convención, cmp toma dos parámetros, self y otro, y retorna 1 si el primer objeto es el mayor, -1 si el segundo objeto es el mayor, y 0 si ambos son iguales.

Algunos tipos están completamente ordenados, lo que significa que se pueden comparar dos elementos cualesquiera y decir cual es el mayor. Por ejemplo, los números enteros y los números en coma flotante tienen un orden completo.

Algunos conjuntos no tienen orden, o sea, que no existe ninguna manera significativa de decir que un elemento es mayor a otro. Por ejemplo, las frutas no tienen orden, lo que explica por que no se pueden comparar peras con manzanas.

El conjunto de los naipes tiene un orden parcial, lo que significa que algunas veces se pueden comparar los naipes, y otras veces no. Por ejemplo, usted sabe que el 3 de Tréboles es mayor que el 2 de Tréboles y el 3 de Diamantes es mayor que el 3 de Tréboles. Pero, >cual es mejor?, >el 3 de Tréboles o el 2 de Diamantes?. Uno tiene mayor valor, pero el otro tiene mayor palo.A los fines de hacer que los naipes sean comparables, se debe decidir que es mas importante: valor o palo.

Para no mentir, la selección es arbitraria. Como algo hay que elegir, diremos que el palo es mas importante, porque un mazo nuevo viene ordenado con todos los Tréboles primero, luego con todos los Diamantes,
y así sucesivamente.

Con esa decisión tomada, podemos escribir cmp :

   1: def __cmp__(self, otro):

   2:     # controlar el palo

   3:     if self.palo > otro.palo: return 1

   4:     if self.palo < otro.palo: return -1

   5:     # si son del mismo palo, controlar el valor

   6:     if self.valor > otro.valor: return 1

   7:     if self.valor < otro.valor: return -1

   8:     # los valores son iguales, es un empate

   9:     return 0

En este ordenamiento, los Ases son menores que los doses.
Como ejercicio, modifique __cmp__ de tal manera que los Ases tengan mayor valor que los Reyes.

15.5. Mazos de naipes

Ahora que ya tenemos los objetos para representar las Cartas, el próximo paso lógico es definir una clase para representar un Mazo. Por supuesto, un mazo esta compuesto de naipes, as³ que cada objeto Mazo contendrá una lista de
naipes como atributo.

A continuación se muestra una definición para la clase Mazo. El metodo de inicialización crea el atributo cartas y genera el conjunto estándar de cincuenta y dos naipes.

   1: class Mazo:

   2:     def __init__(self):

   3:         self.cartas = []

   4:             for palo in range(4):

   5:                 for valor in range(1, 14):

   6:                     self.cartas.append(Carta(palo, valor))

La forma mas fácil de poblar el mazo es mediante un bucle anidado. El bucle exterior enumera los palos desde 0 hasta 3. El bucle interior enumera los valores desde 1 hasta 13. Como el bucle exterior itera cuatro veces, y el interior itera trece veces, la cantidad total de veces que se ejecuta el cuerpo interior es cincuenta y dos (trece por cuatro). Cada iteración crea una nueva instancia de Carta con el palo y valor actual, y agrega dicho naipe a la lista de cartas.

El metodo append funciona sobre listas pero no sobre tuplas, por supuesto.

15.6. Impresión del mazo de naipes

Como es usual, cuando definimos un nuevo tipo de objeto queremos un metodo que imprima el contenido del objeto. Para imprimir un Mazo, recorremos la lista e imprimimos cada Carta:

   1: class Mazo:

   2:     ...

   3:     def muestraMazo(self):

   4:         for carta in self.cartas:

   5:         print carta

 

Desde ahora en adelante, los puntos suspensivos (…) indicaran que hemos omitido los otros métodos en la clase.

En lugar de escribir un metodo muestraMazo, podríamos escribir un metodo __str__ para la clase Mazo. La ventaja de __str__ esta en que es mas flexible. En lugar de imprimir directamente el contenido del objeto, str genera una representación en forma de cadena de caracteres que las otras partes del programa pueden manipular antes de imprimir o almacenar para un uso posterior.

Se presenta ahora una versión de str que retorna una representación como cadena de caracteres de un Mazo. Para darle un toque especial, acomoda los naipes en una cascada, de tal manera que cada naipe esta sangrado un espacio
mas que el precedente.

   1: class Mazo:

   2:     ...

   3:     def __str__(self):

   4:         s = ""

   5:         for i in range(len(self.cartas)):

   6:             s = s + " "*i + str(self.cartas[i]) + "n"

   7:     return s

Este ejemplo demuestra varias características. Primero, en lugar de recorrer self.cartas y asignar cada naipe a una variable, usamos i como variable de bucle e índice de la lista de naipes.

Segundo, utilizamos el operador de multiplicación de cadenas de caracteres para sangrar cada naipe un espacio mas que el anterior. La expresión *i proporciona una cantidad de espacios igual al valor actual de i.

Tercero, en lugar de usar la instrucción print para imprimir los naipes, utilizamos la función str. El pasar un objeto como argumento a str es equivalente a invocar el metodo __str__ sobre dicho objeto.

Finalmente, usamos la variable s como acumulador. Inicialmente, s es una cadena de caracteres vac³a. En cada pasada a través del bucle, se genera una nueva cadena de caracteres que se concatena con el viejo valor de s para obtener el nuevo valor. Cuando el bucle termina, s contiene la representación completa en formato de cadena de caracteres del Mazo, la cual se ve como a continuación se presenta:

   1: >>> mazo = Mazo()

   2: >>> print mazo

   3: As de Treboles

   4: 2 de Treboles

   5:  3 de Treboles

   6:   4 de Treboles

   7:    5 de Treboles

   8:     6 de Treboles

   9:      7 de Treboles

  10:       8 de Treboles

  11:        9 de Treboles

  12:         10 de Treboles

  13:           Sota de Treboles

  14:            Reina de Treboles

  15:              Rey de Treboles

  16:                As of Diamantes

Y así sucesivamente. Aun cuando los resultados aparecen en 52 renglones, se trata de solo una única larga cadena de caracteres que contiene los saltos de línea.

15.7. Barajar el mazo

Si un mazo esta perfectamente barajado, cualquier naipe tiene la misma probabilidad de aparecer en cualquier posición del mazo, y cualquier lugar en el mazo tiene la misma probabilidad de contener cualquier naipe.

Para mezclar el mazo, utilizaremos la función randrange del modulo random. Esta función toma dos enteros como argumentos a y b, y elige un numero entero en forma aleatoria en el rango a <= x <b. Como el l³mite superior es estrictamente menor a b, podemos usar la longitud de la lista como el segundo argumento y de esa manera tendremos garantizado un índice legal dentro de la lista. Por ejemplo, esta expresión selecciona el índice de un naipe al azar dentro del mazo:

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

Una manera sencilla de mezclar el mazo es recorrer los naipes e intercambiar cada una con otra elegida al azar. Es posible que el naipe se intercambie consigo mismo, pero no es un problema. De hecho, si eliminamos esa posibilidad, el orden de los naipes no será completamente al azar:

   1: class Mazo:

   2:     ...

   3:     def mezclar(self):

   4:         import random

   5:         nCartas = len(self.cartas)

   6:         for i in range(nCartas):

   7:             j = random.randrange(i, nCartas)

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

   9:                 self.cartas[j], self.cartas[i]

En lugar de presuponer que hay cincuenta y dos naipes en el mazo, obtenemos la longitud real de la lista y la almacenamos en nCartas.

Para cada naipe del mazo, seleccionamos un naipe al azar entre aquellos que no han sido intercambiados aun. Luego intercambiamos el naipe actual (i) con el naipe seleccionado (j). Para intercambiar los naipes usaremos la asignación de tuplas, como se describe en la Sección 9.2:

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

 

Como ejercicio, reescriba esta línea de código sin usar una asignación de secuencias.

15.8. Eliminación y reparto de los naipes

Otro metodo que podría ser útil para la clase Mazo es eliminaCarta, que toma un naipe como parámetro, lo elimina, y retorna verdadero (1) si el naipe estaba en el mazo, y falso (0) si no estaba:

   1: class Mazo:

   2:     ...

   3:     def eliminaCarta(self, carta):

   4:         if carta in self.cartas:

   5:             self.cartas.remove(carta)

   6:             return 1

   7:         else:

   8:             return 0

El operador in retorna verdadero si el primer operando esta en el segundo, el cual debe ser una lista o tupla. Si el primer operando es un objeto, Python usa el metodo __cmp__ del objeto para determinar la igualdad entre los elementos de la lista. Como el __cmp__ en la clase Carta verifica la igualdad en profundidad, el metodo eliminaCarta también verifica igualdad en profundidad.

Para repartir los naipes, queremos eliminar y devolver el naipe que ocupa la posición superior en el mazo. El metodo pop de las listas proporciona una manera conveniente de realizar esto:

   1: class Mazo:

   2:     ...

   3:     def darCarta(self):

   4:         return self.cartas.pop()

En realidad, pop elimina el ultimo naipe en la lista, así que en efecto estamos repartiendo desde el extremo inferior del mazo.
Otra operación mas que es muy probable necesitemos es la función booleana estaVacio, la cual devuelve verdadero si el mazo no contiene ningún naipe:

   1: class Deck:

   2:     ...

   3:     def estaVacio(self):

   4:         return (len(self.cartas) == 0)

 

 

 

15.9. Glosario

codificar: Representar un conjunto de valores utilizando otro conjunto de valores, entre los cuales se construye una correspondencia.

atributo de clase: Una variable que se define dentro de la definición de un clase pero fuera de cualquiera de sus métodos. Los atributos de clase son accesibles desde cualquier metodo de la clase y están compartidos por todas las instancias de la misma.

acumulador: Una variable que se usa en un bucle para acumular una serie de valores, por ejemplo concatenándolos dentro de una cadena de caracteres o adicionándolos a una suma.

Herencia

16.1. Herencia

La característica de un lenguaje que mas se asocia con la programación orientada a objetos es la herencia. La herencia es la capacidad de definir una nueva clase que es una versión modificada de otra ya existente.

La principal ventaja de esta característica es que se pueden agregar nuevos métodos a una clase sin modificar la clase existente. Se denomina “herencia” porque la nueva clase hereda todos los métodos de la clase existente. Si extendemos
esta metáfora, a la clase existente a veces se la denomina clase padre. La nueva clase puede denominarse clase hija, o también “subclase”.

La herencia es una característica poderosa. Ciertos programas que serían complicados sin herencia pueden escribirse de manera simple y concisa gracias a ella.

Además, la herencia puede facilitar la reutilización del código, pues se puede adaptar el comportamiento de la clase padre sin tener que modificarla. En algunos casos, la estructura de la herencia refleja la propia estructura del problema, Lo que hace que el programa sea mas fácil de comprender.

Por otro lado, la herencia pude hacer que los programas sean difíciles de leer.

Cuando se llama a un metodo, a veces no esta claro donde debe uno encontrar su definición. El código relevante puede estar diseminado por varios módulos.

Además, muchas de las cosas que se hacen mediante el uso de la herencia, se pueden lograr de forma igualmente (incluso mas) elegante sin ella. Si la estructura general del problema no nos gu³a hacia la herencia, dicho estilo de programación
puede hacer mas mal que bien.

16.2. Una mano de cartas

Para casi cualquier juego de naipes, necesitamos representar una mano de cartas.

Una mano es similar a un mazo, por supuesto. Ambos están compuestos de un conjunto de naipes, y ambos requieren de operaciones tales como agregar y eliminar una carta. Además, necesitaremos la capacidad de mezclar tanto un
mazo como una mano de cartas.

Una mano es diferente de un mazo en ciertos aspectos. Según el juego al que se este jugando, podemos querer realizar ciertas operaciones sobre una mano que no tienen sentido sobre un mazo. Por ejemplo, en el póker queremos clasificar una mano (straight (consecutiva), flush (de un solo palo), etc.) y compararla con otra. En bridge necesitaremos calcular el puntaje para la mano para as³ poder hacer la subasta.

Esta situación sugiere el uso de la herencia. Si Mano es una subclase de Mazo, entonces tendrá todos los métodos de Mazo y le podremos agregar otros métodos nuevos.

En la definición de clase, el nombre de la clase padre aparece entre paréntesis:

   1: class Mano(Mazo):

   2:     pass

Esta sentencia indica que la nueva clase Mano hereda de la clase existente Mazo.

El constructor de Mano inicializa los atributos para la mano, que son nombre y cartas. La cadena de caracteres nombre identifica a esta mano, probablemente mediante el nombre del jugador que la sostiene. El nombre es un parámetro opcional con un valor por omisión de cadena vacía. cartas es la lista de cartas de la mano, inicializada como lista vacía.

   1: class Mano(Mazo):

   2:     def __init__(self, nombre=""):

   3:         self.cartas = []

   4:         self.nombre = nombre

 

Casi para cualquier juego de naipes, es necesario agregar y quitar cartas del mazo. La eliminación de cartas ya ha sido resuelta, pues Mano hereda eliminaCarta de Mazo. Pero deberemos escribir agregaCarta:

   1: lass Mano(Mazo):

   2:     ...

   3:     def agregaCarta(self,carta) :

   4:     self.cartas.append(carta)

De nuevo, los puntos suspensivos indican que hemos omitido los otros métodos.

El metodo de lista append agrega la nueva carta al final de la lista de cartas.

16.3. El reparto de los naipes

Ahora que ya tenemos la clase Mano, queremos repartir las cartas del Mazo en manos. No es claramente obvio si este metodo debe ir en la clase Mano o en la clase Mazo, pero como opera sobre un mazo único y (posiblemente) sobre varias manos, es mas natural ponerlo en el Mazo. repartir debe ser bastante general, pues los diferentes juegos tienen distintos requerimientos.

Puede que necesitemos repartir todo el mazo de una vez, o que agreguemos una carta a cada mano.
Repartir toma dos parámetros, una lista (o tupla) de manos y la cantidad total de naipes a repartir. Si no hay suficientes cartas en el mazo, el metodo reparte todas las cartas y se detiene:

   1: class Mazo :

   2:     ...

   3:     def repartir(self, manos, nCartas=999):

   4:     nManos = len(manos)

   5:         for i in range(nCartas):

   6:         if self.estaVacio(): break # fin si se acaban las cartas

   7:         carta = self.darCarta() # da la carta superior

   8:         mano = manos[i % nManos] # a qui¶en le toca?

   9:         mano.agregaCarta(carta) # agrega la carta a la mano

 

El segundo parámetro, nCartas es opcional; el valor por omisión es un numero muy grande, lo cual es lo mismo que decir que se repartirán todos los naipes del mazo.

La variable de bucle i va desde 0 hasta nCartas-1. A cada paso a través del bucle, se elimina una carta del mazo mediante el metodo de lista pop, que quita y devuelve el ultimo elemento de la lista.

El operador modulo ( %) permite que podamos repartir las cartas de una en una (una carta cada vez para cada mano). Cuando i es igual a la cantidad de manos en la lista, la expresión i % nManos salta hacia el comienzo de la lista (el índice
es 0).

Página 122 de 143

Creado con WordPress & Tema de Anders Norén