Cap. 23 - Herencia

23.1) Herencia

El concepto que más frecuentemente se asocia con la programación orientada a objetos es el de herencia.

La ventaja principal de esta funcionalidad es que puedes agregar métodos a la nueva clase sin modificar la original.

La herencia es una funcionalidad poderosa.

Por otro lado, la herencia puede dificultar la comprensión del código de un programa.

En este capítulo mostraremos el uso de herencia como parte de un programa que juega al juego Old Maid (similar al Culo Sucio de la baraja española).

23.2) Una mano de cartas

En casi todos los juegos de cartas es necesario poder representar una mano de cartas.

Una mano también es diferente que un mazo.

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

En este capítulo agregaremos código a nuestro archivo Cartas.py del capítulo anterior (L23_cartas.py).

class Mano(Mazo):
    pass

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

El constructor de Mano inicializa los atributos para la mano, que son nombre y cartas.

class Mano(Mazo):
    def __init__(self, nombre=""):
       self.cartas = []
       self.nombre = nombre

En casi todo juego de cartas es necesario tomar y dejar cartas para agregarlas y quitarlas de una mano.

class Mano(Mazo):
    . . .
    def add(self, carta):
        self.cartas.append(carta)

Como siempre, los 3 puntos suspensivos indican que hemos omitido otros métodos de la clase. El método append de lista agrega una nueva carta al final de la lista de cartas.

23.3) Repartiendo cartas

Ahora que tenemos una clase Mano, queremos repartir cartas del Mazo en varias manos.

El método repartir debería ser suficientemente general, ya que los distintos juegos tienen diversas formas de repartir.

El método repartir toma dos parámetros, una lista (o tupla) de manos y el número total de cartas a repartir.

class Mazo:
    . . .
    def esta_vacio(self):
        return self.cartas == []

    def repartir(self, manos, cantidad_cartas=999):
        cantidad_manos = len(manos)
        for i in range(cantidad_cartas):
            if self.esta_vacio():
                break                       # Break si no quedan cartas
            carta = self.pop()              # Tomar la carta de arriba
            mano = manos[i % cantidad_manos]  # De quién es el próximo turno?
            mano.add(carta)                 # Agregar la carta a la mano

El segundo parámetro, cantidad_cartas, es opcional: el valor por defecto es un número muy grande, lo que implica que se reparten todas las cartas del mazo.

La variable i del loop va desde 0 hasta cantidad_cartas - 1. En cada iteración se quita una carta del mazo mediante el método pop, que elimina y devuelve el último elemento de la lista.

El operador módulo (%) nos permite repartir cartas en ronda (una carta por vez a cada mano).

23.4) Imprimiendo una mano

Para imprimir el contenido de una mano, podemos aprovechar el método __str__ heredado de Mazo. Por ejemplo:

>>> mazo = Mazo()
>>> mazo.mezclar()
>>> mano = Mano("Franco")
>>> mazo.repartir([mano], 5)
>>> print(mano)
7 de Picas
 9 de Tréboles
  7 de Tréboles
   5 de Tréboles
    Sota de Diamantes

No es una gran mano, pero podría dar pie a una escalera de tréboles.

Si bien es conveniente heredar los métodos existentes, hay información adicional en un objeto Mano que también podemos querer imprimir.

class Mano(Mazo):
    . . .
    def __str__(self):
        s = "Mano " + self.nombre
        if self.esta_vacio():
            s += " está vacía\n"
        else:
            s += " contiene\n"
        return s + Mazo.__str__(self)

Al principio, s es un string que identifica la mano.

Puede parecer extraño enviar a self (que se refiere a una mano) al método de la clase Mazo, pero debemos recordar que definimos una Mano como un tipo de Mazo.

En general, siempre es legal usar una instancia de una subclase en lugar de una instancia de una clase ancestro.

23.5) La clase JuegoCartas

La clase JuegoCartas se ocupa de algunos aspectos centrales comunes a casi todos los juegos de cartas, como crear un mazo y mezclarlo.

class JuegoCartas:
    def __init__(self):
        self.mazo = Mazo()
        self.mazo.mezclar()

Este es el primer ejemplo que vemos de un método de inicialización que hace una computación importante, más allá de la mera asignación de valores a los atributos.

Para implementar juegos específicos, podemos heredar de JuegoCartas y agregar funcionalidades al nuevo juego. Como ejemplo, escribiremos una simulación de la Solterona.

El objetivo de la Solterona es deshacerte de las cartas que tienes en tu mano. Lo haces mediante coincidencias de valor y palo.

Al comenzar el juego, la Reina de Tréboles es removida del mazo, por lo cual la Reina de Picas no tiene correspondiente.

Cuando ya no se pueden hacer pares, comienza el juego. Por turno, cada jugador toma una carta (sin verla) de su vecino sentado a la izquierda (el primero que todavía tenga cartas).

En nuestra simulación computarizada del juego, la computadora jugará todas las manos.

23.6) La clase ManoSolterona (OldMaidHand)

Una mano para jugar La Solterona requiere ciertas capacidades que van más allá de las de una mano normal.

class ManoSolterona(Mano):
    def descartar_pares(self):
        cuenta = 0
        cartas_iniciales = self.cartas[:]
        for carta in cartas_iniciales:
            match = Carta(3 - carta.palo, carta.valor)
            if match in self.cartas:
                self.cartas.remove(carta)
                self.cartas.remove(match)
                print("Mano {0}: {1} se corresponde con {2}"
                        .format(self.nombre, carta, match))
                cuenta += 1
        return cuenta

Comenzamos por hacer una copia de la lista de cartas, para poder ir recorriendo la copia al mismo tiempo que vamos eliminando cartas de la lista original.

Para cada carta en la mano, averiguamos primero cuál sería su carta correspondiente (match).

El siguiente ejemplo muestra cómo se usa descartar_pares:

>>> juego = JuegoCartas()
>>> mano = ManoSolterona("Franco")
>>> juego.mazo.repartir([mano], 13)
>>> print(mano)
Mano Franco contiene
7 de Diamantes
 6 de Picas
  2 de Picas
   Sota de Corazones
    Sota de Tréboles
     Sota de Diamantes
      Rey de Picas
       Rey de Tréboles
        5 de Diamantes
         10 de Corazones
          Reina de Corazones
           5 de Picas
            3 de Diamantes

>>> mano.descartar_pares()
Mano Franco: Sota de Corazones se corresponde con Sota de Diamantes
Mano Franco: Rey de Picas se corresponde con Rey de Tréboles
2
>>> print(mano)
Mano Franco contiene
7 de Diamantes
 6 de Picas
  2 de Picas
   Sota de Tréboles
    5 de Diamantes
     10 de Corazones
      Reina de Corazones
       5 de Picas
        3 de Diamantes

Observar que la clase ManoSolterona no tiene método __init__ (no le hemos implementado uno).

23.7) La clase JuegoSolterona (OldMaidGame)

Ahora podemos ocuparnos del juego propiamente dicho.

  • JuegoSolterona será una subclase de JuegoCartas con un nuevo método llamado jugar que recibe una lista de jugadores como parámetro.
  • Como __init__ es heredado de JuegoCartas, un nuevo objeto JuegoSolterona contiene automáticamente un mazo mezclado:

class JuegoSolterona(JuegoCartas):
    def jugar(self, nombres):
        # Eliminar Reina de Tréboles
        self.mazo.remover(Carta(0,12))

        # Crear una mano para cada jugador
        self.manos = []
        for nombre in nombres:
            self.manos.append(ManoSolterona(nombre))

        # Repartir las cartas
        self.mazo.repartir(self.manos)
        print("---------- Las cartas fueron repartidas")
        self.print_manos()

        # Eliminar pares coincidentes iniciales
        matches = self.descartar_todos_los_pares()
        print("---------- Descartados los pares inicialies, comienza el juego")
        self.print_manos()

        # Jugar hasta que las 50 cartas se hayan descartado
        turno = 0
        cantidad_manos = len(self.manos)
        while matches < 25:
            matches += self.jugar_un_turno(turno)
            turno = (turno + 1) % cantidad_manos

        print("---------- Fin del Juego")
        self.print_manos()

Dejamos como ejercicio la implementación de print_manos().

  • Respuesta: implementación de print_manos:
  •     def print_manos(self):
            for mano in self.manos:
                print(mano)

Algunos de los pasos del juego fueron separados en métodos. Por ejemplo descartar_todos_los_pares recorre la lista de manos y llama a descartar_pares para cada una:

class JuegoSolterona(JuegoCartas):
    . . .
    def descartar_todos_los_pares(self):
        cantidad = 0
        for mano in self.manos:
            cantidad += mano.descartar_pares()
        return cantidad

La variable cantidad es un acumulador que lleva la cuenta de la cantidad de pares descartados en cada mano. Cuando hemos recorrido todas las manos, se retorna el valor total (cantidad).

Cuando la cantidad total de pares descartados (matches) llega a 25, sabemos que 50 cartas han sido quitadas del mazo y por lo tanto sólo queda una carta: el juego ha terminado.

La variable turno indica qué jugador tiene el turno de jugar.

  • Comienza valiendo 0 y se incrementa de a uno. Cuando alcanza cantidad_manos el operador módulo lo baja nuevamente a 0.

El método jugar_un_turno recibe un parámetro que indica de quién es el turno. El valor de retorno es la cantidad de pares descartados durante este turno.

class JuegoSolterona(JuegoCartas):
    . . .
    def jugar_un_turno(self, i):
        if self.manos[i].esta_vacio():
            return 0
        vecino = self.encontrar_vecino(i)
        carta_tomada = self.manos[vecino].pop()
        self.manos[i].add(carta_tomada)
        print("Mano", self.manos[i].nombre, "tomó", carta_tomada)
        cantidad = self.manos[i].descartar_pares()
        self.manos[i].mezclar()
        return cantidad

Si la mano de un jugador está vacía, ese jugador ya no puede jugar, por lo que no hace nada y retorna 0.

En caso contrario, un turno consiste en encontrar al primer jugador a la izquierda que tenga cartas, tomar una carta del mismo, y chequear si hay pares que se puedan descartar ahora en mi mano.

  • Antes de retornar, las cartas en la mano son mezcladas de forma que la elección del siguiente jugador sea aleatoria.

El método encontrar_vecino comienza con el jugador que está inmediatamente a la izquierda y continúa circulando hasta encontrar al primer jugador que todavía tenga cartas:

class JuegoSolterona(JuegoCartas):
    . . .
    def encontrar_vecino(self, i):
        cantidad_manos = len(self.manos)
        for proximo in range(1,cantidad_manos):
            vecino = (i + proximo) % cantidad_manos
            if not self.manos[vecino].esta_vacio():
                return vecino

Si encontrar_vecino llegara a recorrer todo el círculo de jugadores sin encontrar vecino, retornaría None y provocaría un error en algún paso posterior del programa.

  • Afortunadamente, se puede probar que eso nunca ocurrirá (siempre y cuando el fin del juego se detecte correctamente)
  • La razón: la única forma de no encontrar vecinos con cartas es que sólo un jugador tenga cartas, y es fácil probar que en tal caso esa carta sería la Solterona y el juego ya habría terminado.

El siguiente output es el de una corrida completa del juego para tres jugadores:

>>> juego = JuegoSolterona()
>>> juego.jugar(["Quico", "Chavo", "Chilindrina"])
---------- Las cartas fueron repartidas
Mano Quico contiene
8 de Diamantes
 4 de Tréboles
  8 de Tréboles
   4 de Diamantes
    Reina de Diamantes
     Rey de Picas
      Rey de Corazones
       7 de Diamantes
        6 de Tréboles
         10 de Tréboles
          Rey de Tréboles
           6 de Corazones
            As de Diamantes
             8 de Picas
              2 de Diamantes
               4 de Corazones
                7 de Corazones

Mano Chavo contiene
5 de Diamantes
 7 de Tréboles
  4 de Picas
   10 de Corazones
    3 de Tréboles
     10 de Diamantes
      8 de Corazones
       5 de Corazones
        Rey de Diamantes
         6 de Picas
          3 de Corazones
           Sota de Tréboles
            As de Tréboles
             Reina de Corazones
              9 de Tréboles
               5 de Picas
                Sota de Corazones

Mano Chilindrina contiene
6 de Diamantes
 Sota de Diamantes
  7 de Picas
   9 de Diamantes
    3 de Picas
     2 de Picas
      As de Corazones
       9 de Corazones
        Sota de Picas
         2 de Tréboles
          3 de Diamantes
           10 de Picas
            2 de Corazones
             5 de Tréboles
              9 de Picas
               Reina de Picas
                As de Picas

Mano Quico: 8 de Tréboles se corresponde con 8 de Picas
Mano Quico: 4 de Diamantes se corresponde con 4 de Corazones
Mano Quico: Rey de Picas se corresponde con Rey de Tréboles
Mano Quico: 7 de Diamantes se corresponde con 7 de Corazones
Mano Chavo: 5 de Diamantes se corresponde con 5 de Corazones
Mano Chavo: 10 de Corazones se corresponde con 10 de Diamantes
Mano Chilindrina: 9 de Diamantes se corresponde con 9 de Corazones
Mano Chilindrina: 2 de Picas se corresponde con 2 de Tréboles
---------- Descartados los pares inicialies, comienza el juego
Mano Quico contiene
8 de Diamantes
 4 de Tréboles
  Reina de Diamantes
   Rey de Corazones
    6 de Tréboles
     10 de Tréboles
      6 de Corazones
       As de Diamantes
        2 de Diamantes

Mano Chavo contiene
7 de Tréboles
 4 de Picas
  3 de Tréboles
   8 de Corazones
    Rey de Diamantes
     6 de Picas
      3 de Corazones
       Sota de Tréboles
        As de Tréboles
         Reina de Corazones
          9 de Tréboles
           5 de Picas
            Sota de Corazones

Mano Chilindrina contiene
6 de Diamantes
 Sota de Diamantes
  7 de Picas
   3 de Picas
    As de Corazones
     Sota de Picas
      3 de Diamantes
       10 de Picas
        2 de Corazones
         5 de Tréboles
          9 de Picas
           Reina de Picas
            As de Picas

Mano Quico tomó Sota de Corazones
Mano Chavo tomó As de Picas
Mano Chavo: As de Tréboles se corresponde con As de Picas
Mano Chilindrina tomó 10 de Tréboles
Mano Chilindrina: 10 de Picas se corresponde con 10 de Tréboles
Mano Quico tomó 5 de Picas
Mano Chavo tomó 9 de Picas
Mano Chavo: 9 de Tréboles se corresponde con 9 de Picas
Mano Chilindrina tomó 8 de Diamantes
Mano Quico tomó 8 de Corazones
Mano Chavo tomó 2 de Corazones
Mano Chilindrina tomó 6 de Corazones
Mano Chilindrina: 6 de Diamantes se corresponde con 6 de Corazones
Mano Quico tomó 6 de Picas
Mano Quico: 6 de Tréboles se corresponde con 6 de Picas
Mano Chavo tomó As de Corazones
Mano Chilindrina tomó 4 de Tréboles
Mano Quico tomó 3 de Corazones
Mano Chavo tomó 3 de Picas
Mano Chavo: 3 de Tréboles se corresponde con 3 de Picas
Mano Chilindrina tomó 2 de Diamantes
Mano Quico tomó Rey de Diamantes
Mano Quico: Rey de Corazones se corresponde con Rey de Diamantes
Mano Chavo tomó 5 de Tréboles
Mano Chilindrina tomó 5 de Picas
Mano Quico tomó 7 de Tréboles
Mano Chavo tomó 2 de Diamantes
Mano Chavo: 2 de Corazones se corresponde con 2 de Diamantes
Mano Chilindrina tomó 3 de Corazones
Mano Chilindrina: 3 de Diamantes se corresponde con 3 de Corazones
Mano Quico tomó Reina de Corazones
Mano Quico: Reina de Diamantes se corresponde con Reina de Corazones
Mano Chavo tomó Reina de Picas
Mano Chilindrina tomó As de Diamantes
Mano Quico tomó Reina de Picas
Mano Chavo tomó 4 de Tréboles
Mano Chavo: 4 de Picas se corresponde con 4 de Tréboles
Mano Chilindrina tomó Reina de Picas
Mano Quico tomó As de Corazones
Mano Chavo tomó Sota de Diamantes
Mano Chilindrina tomó 8 de Corazones
Mano Chilindrina: 8 de Diamantes se corresponde con 8 de Corazones
Mano Quico tomó 5 de Tréboles
Mano Chavo tomó As de Diamantes
Mano Chilindrina tomó 5 de Tréboles
Mano Chilindrina: 5 de Picas se corresponde con 5 de Tréboles
Mano Quico tomó Sota de Diamantes
Mano Quico: Sota de Corazones se corresponde con Sota de Diamantes
Mano Chavo tomó 7 de Picas
Mano Chilindrina tomó 7 de Tréboles
Mano Quico tomó As de Diamantes
Mano Quico: As de Corazones se corresponde con As de Diamantes
Mano Chavo tomó Sota de Picas
Mano Chavo: Sota de Tréboles se corresponde con Sota de Picas
Mano Chilindrina tomó 7 de Picas
Mano Chilindrina: 7 de Tréboles se corresponde con 7 de Picas
---------- Fin del Juego
Mano Quico está vacía

Mano Chavo está vacía

Mano Chilindrina contiene
Reina de Picas

Así que perdió la Chilindrina.

23.8) Glosario

  • herencia, clase padre/ancestro, subclase

23.9) Ejercicios

1) Agregar un método print_manos a la clase JuegoSolterona que recorra self.manos e imprima cada mano.

 

2) Define un nuevo tipo de Turtle (tortuga), TurtleGTX, que venga con las siguientes funcionalidades extra:

  • Puede saltar a cierta distancia, y tiene un odómetro que lleva la cuenta de qué tan lejos se ha movido la tortuga desde que salió de la línea de producción.
  • (La clase padre tiene varios sinónimos como fd, forward, back, backward y bk: para este ejercicio enfócate en poner esta funcionalidad dentro del método forward)
  • Piensa con cuidado cómo contar la distancia si se le pide a la tortuga que avance (forward) una cantidad negativa.
  • (No quisiéramos comprar una tortuga de segunda mano cuyo odómetro nos engañara porque el dueño anterior solía moverse en reversa. Puedes probarlo con un auto de verdad, y fijarte si el odómetro cuenta hacia arriba o hacia abajo cuando se va en reversa)
  • (La respuesta es que el odómetro siempre cuenta hacia arriba, porque lo que se recorra se recorre, sea para donde sea: o sea que un movimiento en reversa ha de contarse positivo)

 

3) Luego de viajar cierta distancia al azar, tu tortuga debería detenerse por una rueda pinchada.

  • Cuando esto ocurra, lanza una excepción donde sea que forward sea llamada.
  • También deberás proveer un método cambiar_rueda que permita corregir el pinchazo.