Siguiente Subir Anterior Guía básica para pensar como un informático Índice

Capítulo 16

Comentarios

Herencia

16.1 Herencia

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

16.2 Mano de cartas

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

16.3 Repartir las cartas

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

16.4 Mostrar en pantalla una Mano

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

16.5 La clase JuegoDeCartas

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

16.6 La clase OldMaidMano

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

16.7 La clase OldMaidJuego

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

16.8 Glosario

Herencia
Posiblidad de definir una nueva clase que se trate de una versión modificada de una clase definida previamente.
clase madre
Aquella clase a partir de la cual se hereda la clase hija.
clase hija
Nueva clase heredada de una clase existente; se llama también "subclase".


Siguiente Subir Anterior Hola Índice