![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
La herencia es la propiedad del lenguaje que con mayor frecuencia se asocia a la programación orientada a objetos. Se puede definir como la capacidad para definir una clase nueva, que es una versión modificada de una clase ya existente.
Su ventaja principal es que puedes añadir métodos nuevos a una clase, sin modificar la clase ya existente. Se la llama "herencia" porque la clase nueva hereda todos los métodos de la clase ya existente. Con frecuencia, si se amplía esta metáfora, la clase existente se puede llamar clase madre y la clase nueva se puede llamar clase hija o "subclase".
Se trata de una propiedad potente. Así, programas que resultarían complicados si no tuvieran herencia, se escriben de forma concisa y sencilla cuando cuentan con ella. Además, la herencia puede facilitar la reutilización del código, pues se puede individualizar el comportamiento de las clases madre sin tener que modificarlas; y, en ciertas ocasiones, la estructura de la herencia refleja la estructura natural del problema, lo que hace que el programa resulte más fácil de entender.
Asimismo, la herencia puede dificultar la lectura de programas. Con frecuencia, cuando se invoca un método, no queda claro dónde podemos encontrar su definición y el código relevante puede estar disperso entre varios módulos. Además, muchas de las acciones que se pueden efectuar mediante la herencia, también se pueden realizar de una forma muy elegante (en cierta medida) sin ella. Si la estructura natural del problema no se presta por sí misma a la herencia, este estilo de programación puede resultar al final más negativo que positivo.
En este capítulo, mostraremos la utilización de la herencia como parte de un programa que contiene el juego de cartas Old Maid y uno de nuestros objetivos consistirá en escribir un código que pueda reutilizarse para crear otros juegos de cartas.
Comentarios
En la mayoría de juegos de cartas, necesitamos representar lo que es una mano de cartas. La mano es similar al mazo de la baraja. Ámbas se conforman a partir de un juego de cartas, implican operaciones de añadir y eliminar cartas, y, finalmente, podríamos resaltar la capacidad para barajar barajas y manos.
Ahora bien, una mano es diferente de una baraja. Así pues, dependiendo del juego, podríamos realizar alguna operación con las manos que no tiene porqué realizarse en un mazo de baraja. En el póker, por ejemplo, podríamos clasificar la mano como escalera o escalera de color, o podríamos comparararla con otra mano. Y en el bridge, podríamos querer alcanzar una puntuación determinada en una mano, para poder hacer una apuesta.
Esta situación sugiere la utilización de la herencia. Por ello, si la Mano es una subclase de Baraja, la Mano tendrá todos los métodos de Baraja y además podrán añadirse métodos nuevos.
En la definición de clase, el nombre de la clase madre se coloca entre paréntesis:
class Mano (Baraja):
pass
Esta sentencia indica que la nueva clase Mano hereda de la clase Baraja existente.
El constructor de Mano inicializa que los atributos para la mano sean nombre y cartas. La cadena nombre identifica esta mano, seguramente, por el nombre del jugador que la sostiene. El nombre es un parámetro opcional que tiene como valor predeterminado la cadena vacía. Y el atributo cartas es la lista de cartas de la mano, inicializada en la lista vacía:
class Mano (Baraja):
def __init__(self, nombre=""):
self.cartas = []
self.nombre = nombre
Finalmente, en la mayoría de los juegos de cartas, se considera necesario añadir o eliminar cartas de la baraja. La eliminación de las cartas se lleva cabo desde el momento en que la Mano hereda eliminarCarta de laBaraja. No obstante, tenemos que escribir agregarCarta:
class Mano (Baraja):
...
def agregarCarta (self,carta) :
self.cartas.append (carta)
De nuevo, la elípsis indica que hemos omitido otros métodos y el método de lista append añade la carta nueva al final de la lista de cartas.
Comentarios
Ahora que ya tenemos una clase Mano, queremos repartir las cartas de la Baraja a las manos. No está directamente claro si este método pertenece a la claseMano o a la clase Baraja. Pero, como opera en un solo mazo y, posiblemente, en varias manos, es más común colocarlo en Baraja.
repartir debe ser un proceso bastante general, ya que dependiendo del juego se establecerán reglas diferentes. Algunas veces, querremos repartir todas las cartas y otras veces, añadir una carta a cada mano.
repartir tiene dos parámetros, una lista de las cartas (o tupla) y el número total de cartas que se va a repartir. Si no hubiera suficientes cartas en la baraja, el método reparte todas las cartas y para:
clase Baraja :
...
def repartir(self, manos, nCartas=999):
nManos = len(manos)
for i in rango(nCartas):
if self.estaVacia(): cortar # cortar si no quedan más cartas
carta = self.popCarta() # tomar la primera carta encima de la baraja
mano = manos[i % nManos] #¿A quién le toca?
mano.agregarMano(carta) # agregar la carta a la mano
El segundo parámetro, nCartas, es opcional; el valor predeterminado es un número grande, lo que, efectivamente, significa que se repartirán todas las cartas del mazo.
La variable bucle i va del 0 al nCartas-1. Cada vez que se pase por el blucle, se elimina una carta de la baraja utilizando el método de la lista pop, que elimina y devuleve el último objeto de la lista.
El operador módulo (%) nos permite repartir las cartas en círculo (una carta a la vez para cada mano). Cuando i equivale al número de manos de la lista, la expresión i % nManos vuelve al principio de la lista (índice 0).
Comentarios
Para imprimir los contenidos de una mano, nos podemos aprovechar de los métodos imprimirBaraja y __str__ que se heredaron de la Baraja. Por ejemplo,
>>> baraja = Baraja()
>>> baraja.barajar()
>>> mano = Mano("Juan")
>>> baraja.repartir([mano], 5)
>>> print mano
Mano Juan contiene
2 de Picas
3 de Picas
4 de Picas
As de Corazones
9 de Tréboles
No es una gran mano, pero tiene todas las cartas para hacer escalera de color.
Aunque sea conveniente heredar los métodos ya existentes, hay información adicional en el objeto Mano que puede que queramos incluir cuando lo mostremos en una pantalla. Para realizar esto, podemos proveer un método __str__ en la clase Mano que se imponga al de la clase Baraja:
clase Mano(Baraja)
...
def __str__(self):
s = "Mano " + self.nombre
if self.estáVacía():
s = s + " está vacía\n"
else:
s = s + " contiene\n"
return s + Baraja.__str__(self)
Al principio, s es una cadena que identifica la mano. Si la mano está vacía, el programa añade las palabras está vacía y devuelve s.
En el caso contrario, el programa añade la palabra contiene y la representación de tipo cadena de la Baraja, que se computa invocando al método __str__ en la clase Baraja con self.
Puede resultar extraño enviar self, que hace referencia a una Mano actual, al método Baraja. Pero, rápidamente, se cae en la cuenta de que una Mano es un tipo de Baraja. Los objetos Mano pueden realizar todo lo que realizan los objetos Baraja, de modo que, es válido enviar una Mano a un método Baraja.
Por regla general, siempre es válido utilizar una instancia de una subclase en el lugar de una instancia de una clase madre.
Comentarios
La clase JuegoDeCartas se encarga de varias tareas básicas de todos los juegos como preparar una baraja y mezclarla:
class JuegoDeCartas:
def __init__(self):
self.baraja = Baraja()
self.baraja.barajar()
Este es el primer caso que hemos visto en el que el método de inicializacion lleva a cabo una computación significativa, aparte de inicializar atributos.
Para llevar a cabo juegos específicos, podemos heredar de JuegoDeCartas y añadir características para el nuevo juego. Como ejemplo, vamos a escribir un simulacro del juego Old Maid.
La finalidad de Old Maid es descartarse. Para hacerlo, debe hacer parejas con las cartas por categoría y color. Por ejemplo, el 4 de Trébol hace pareja con el 4 de Picas ya que los dos palos son negros. La Jota de Corazones hace pareja con la Jota de Diamantes porque las dos son rojas.
Para comenzar el juego, la Dama de Tréboles se retira de la baraja para que la Dama de Picas no tenga pareja. Las cincuenta y una cartas restantes se reparten entre los jugadores en círculo. Después del reparto, todos los jugadores empiezan a hacer parejas y a descartarse todas las cartas que puedan.
Cuando ya no se puedan hacer más parejas, empieza el juego. Por turnos, cada jugador elige una carta (sin mirar) del vecino más cercano a su izquierda que aún tenga cartas. Si la carta que ha elegido hace pareja con alguna de las cartas que lleva en la mano, se elimina el par. Si no, la carta se añade a la mano del jugador. Al final, se harán todas las parejas y sólo se quedará la Dama de Picas en la mano del perdedor.
En el simulacro informático del juego, el ordenador juega todas las manos. Desgraciadamente, algunos matices del juego real se pierden. En una partida real, el jugador con la carta Old Maid se esfuerza en que su vecino elija esa carta, mostrándola un poco más, escondiéndola un poco, o incluso, negándose a enseñarla completamente. El ordenador simplemente elige al azar una carta del vecino.
Comentarios
Una mano para jugar a Old Maid requiere unas habilidades que van más allá de las habilidades generales de una Mano. Vamos a definir una clase nueva, OldMaidMano, que se hereda de Mano y nos aporta un método adicional llamado eliminarParejas:
class OldMaidMano(Mano):
def eliminarParejas(self):
recuento = 0
cartasOriginales = self.cartas[:]
for carta in cartasOriginales:
pareja = Carta(3 - cartas.palo, carta.rango)
if pareja in self.cartas:
self.cartas.remove(carta)
self.cartas.remove(pareja)
print "Mano %s: %s hace pareja con %s" % (self.nombre,carta,pareja)
recuento = recuento + 1
return recuento
En primer lugar, vamos a hacer una copia de la lista de cartas, para poder recorrer la copia mientras sacamos cartas del original. Dado que self.cartas se modifica en un bucle, no vamos a usarlo para controlar el recorrido. ¡Python se puede confundir, si está recorriendo una lista que está cambiando!
Para cada carta de la mano, pensemos con qué carta se puede empezar y la buscamos. Para emparejarse la carta debe tener el mismo rango y otro palo del mismo color. La expresión 3 - carta.palo convierte un Trébol (palo 0) en una Pica (palo 3) y un Diamante (palo 1) en un Corazón (palo 2). Debería sentirse orgulloso de que las operaciones contrarias también funcionen. Si se puede emparejar con otra carta que también esté en la mano, ambas cartas se eliminan.
El siguiente ejemplo nos enseña a utilizar eliminarParejas:
>>> juego = JuegoDeCartas()
>>> mano = OldMaidMano("Juan")
>>> juego.baraja.repartir([mano], 13)
>>> print mano
Mano Juan contiene
As de Picas
2 de Diamantes
7 de Picas
8 de Tréboles
6 de Corazones
8 de Picas
7 de Tréboles
Dama de Tréboles
7 de Diamantes
5 de Tréboles
Jota de Diamantes
10 de Diamantes
10 de Corazones
>>> mano.eliminarParejas()
Mano Juan: 7 de Picas hace pareja con 7 de Tréboles
Mano Juan: 8 de Picas hace pareja con 8 de Tréboles
Mano Juan: 10 de Diamantes hace parejas con 10 de Corazones
>>> print mano
Mano Juan contiene
As de Picas
2 de Diamantes
6 de Corazones
Dama de Tréboles
7 de Diamantes
5 de Tréboles
Jota de Diamantes
Observe que no hay un método __init__ para la clase OldMaidMano. Lo heredamos de Mano.
Comentarios
Ahora, vamos a prestar atención al juego en sí. OldMaidJuego es una subclase de JuegoDeCartas con un nuevo método llamado juego, el cual utiliza una lista de jugadores como parámetro.
Como __init__ se hereda de JuegoDeCartas, un nuevo objeto OldMaidJuego contiene una nueva baraja ya barajada:
class OldMaidJuego(JuegoDeCartas):
def juego(self, nombres):
# eliminar Dama de Tréboles
self.baraja.eliminarCarta(Carta(0,12))
# repartir una mano para cada jugador
self.manos = []
for nombre in nombres :
self.manos.append(OldMaidMano(nombre))
# repartir las cartas
self.baraja.repartir(self.manos)
print "---------- Se han repartido las cartas"
self.imprimirManos()
# eliminar parejas iniciales
parejas = self.eliminarTodasLasParejas()
print "---------- Parejas descartadas, comienza el juego"
self.imprimirManos()
# jugar hasta que las 50 cartas se emparejen
turn = 0
numManos = len(self.manos)
while parejas < 25:
parejas = parejas + self.jugarUnTurno(turn)
turno = (turno + 1) % numManos
print "---------- Fin del Juego"
self.imprimirManos()
Algunos de los pasos del juego se han separado en métodos. eliminarTodasLasParejas recorre la lista de manos e invoca eliminarParejas en cada caso:
class OldMaidJuego(JuegoDeCartas):
...
def eliminarTodasLasParejas(self):
recuento = 0
for mano in self.manos:
recuento = recuento + mano.removerParejas()
return recuento
A modo de ejercicio, escribe imprimirManos, que recorre self.manos e imprime cada mano.
recuento es un acumulador que suma el número de parejas de cada mano y da el total.
Cuando el número total de parejas llega a veinticinco, se han eliminado cincuenta cartas de las manos, lo que significa que solamente queda una carta y que el juego ha terminado.
La variable turno lleva la cuenta del turno de los jugadores. Empieza en 0 y aumenta uno cada vez; cuando llega a numManos, el operador módulo la pone de nuevo a 0.
El método jugarUnTurno utiliza un parámetro que indica de quién es el turno. El valor de retorno es el número de parejas conseguidas durante este turno:
class OldMaidJuego(JuegoDeCartas):
...
def jugarUnTurno(self, i):
if self.manos[i].estáVacía():
return 0
vecino = self.buscarVecino(i)
cartaElegida = self.manos[vecino].popCarta()
self.manos[i].añadirCarta(cartaElegida)
print "Mano", self.manos[i].nombre, "eligio", cartaElegida
recuento = self.manos[i].eliminarPartidas()
self.manos[i].barajar()
return recuento
Si la mano de un jugador está vacía, este jugador está fuera del juego. Por lo tanto, este jugador no hace nada y empieza de 0.
Por el contrario, un turno consiste en buscar al primer jugador situado a la izquierda que tenga cartas. Se toma una carta del vecino y se comprueba si se puede hacer parejas. Antes de volver, se barajan las cartas en la mano para que la elección del siguiente jugador sea aleatoria.
El método buscarVecino comienza con el jugador que se encuentra inmediatamente a la izquierda y continua en círculo hasta que se encuentre un jugador que todavía tenga cartas:
class OldMaidJuego(JuegoDeCartas):
...
def buscarVecino(self, i):
numManos = len(self.manos)
for siguiente in range(1,numManos):
vecino = (i + siguiente) % numManos
if not self.manos[vecino].estáVacía():
return vecino
Si buscarVecino alguna vez realizara todo el recorrido sin encontrar cartas, devolvería None y provocaría un error en alguna parte del programa. Afortunadamente, podemos probar que esto nunca pasará (siempre que el final del juego se detecte correctamente).
Hemos omitido el método imprimirManos. Éste puedes escribirlo tú mismo.
La siguiente salida se realiza desde una forma truncada del juego donde solamente tres jugadores se reparten las quince cartas más altas (diez y superiores). Con esta pequeña baraja, el juego se detiene después de siete parejas en lugar de veinticinco.
>>> import cartas
>>> juego = cartas.OldMaidJuego()
>>> juego.play(["Antonio","Jorge","Daniel"])
---------- Se han repartido las cartas
Mano Antonio contiene
Rey de Corazones
Jota de Tréboles
Dama de Picas
Rey de Picas
10 de Diamantes
Mano Jorge contiene
Dama de Corazones
Jota de Picas
Jota de Corazones
Rey de Diamantes
Dama de Diamantes
Mano Daniel contiene
Jota de Diamantes
Rey de Tréboles
10 de Picas
10 de Corazones
10 de Tréboles
Mano Jorge : Dama de Corazones hace pareja con Dama de Diamantes
Mano Daniel : 10 de Picas hace pareja con 10 de Tréboles
---------- Parejas descartadas, comienza el juego
Mano de Antonio contiene
Rey de Corazones
Jota de Tréboles
Dama de Picas
Rey de Picas
10 de Diamantes
Mano Jorge contiene
Jota de Picas
Jota de Corazones
Rey de Diamantes
Mano Daniel contiene
Jota de Diamantes
Rey de Tréboles
10 de Corazones
Mano Antonio eligió Rey de Diamantes
Mano Antonio: Rey de Corazones hace pareja con Rey de Diamantes
Mano Jorge eligió 10 de Corazones
Mano Daniel eligió Jota de Tréboles
Mano Antonio eligió Jota de Corazones
Mano Jorge eligió Jota de Diamantes
Mano Daniel eligió Dama de Picas
Mano Antonio eligió Jota de Diamantes
Mano Antonio: Jota de Corazones hace pareja con Jota de Diamantes
Mano Jorge eligió Rey de Tréboles
Mano Daniel eligió Rey de Picas
Mano Antonio eligió 10 de Corazones
Mano Antonio: 10 de Diamantes hace pareja con 10 de Corazones
Mano Jorge eligió Dama de Picas
Mano Daniel eligió Jota de Picas
Mano Daniel: Jota de Tréboles hace pareja con Jota de Picas
Mano Jorge eligió Rey de Picas
Mano Jorge: Rey de Tréboles hace pareja con Rey de Picas
---------- Fin del Juego
Mano Antonio está vacía
Mano Jorge contiene
Dama de Picas
Mano Daniel está vacía
Entonces, Jorge pierde.
Comentarios
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |