Next Up Previous Hi Index

Chapter 16

Herencia

16.1 Herencia

La característica de un lenguaje que más 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 maté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 más fácil de comprender.

Por otro lado, la herencia pude hacer que los porgramas sean difíciles de leer. Cuando se llama a un método, a veces no está claro dónde 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 más) elegante sin ella. Si la estructura general del problema no nos guía hacia la herencia, dicho estilo de programación puede hacer más mal que bien.

En este capítulo demostraremos el uso de la herencia como parte de un programa que juega a las cartas a la "Mona". Una de nuestras metas será que el código que escribamos se pueda reutilizar para implementar otros juegos de naipes.

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 esté 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 (\eningles{straight (consecutiva), \eningles{flush} (de un solo palo), etc.) y compararla con otra. En \eningles{bridge} necesitaremos calcular el puntaje para la mano para así poder hacer la subasta. Esta situación sugiere el uso de la herencia. Si {\tt Mano} es una subclase de {\tt Mazo}, entonces tendrá todos los métodos de {\tt Mazo} y le podremos agregar otros métodos nuevos. \index{clase padre} \index{clase!padre} En la definición de clase, el nombre de la clase padre aparece entre paréntesis: \beforeverb \begin{verbatim} class Mano(Mazo): pass \end{verbatim} \afterverb % Esta sentencia indica que la nueva clase {\tt Mano} hereda de la clase existente {\tt Mazo}. El constructor de {\tt Mano} inicializa los atributos para la mano, que son {\tt nombre} y {\tt cartas}. La cadena de caracteres {\tt 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. {\tt cartas} es la lista de cartas de la mano, inicializada como lista vacía. \beforeverb \begin{verbatim} class Mano(Mazo): def __init__(self, nombre=""): self.cartas = [] self.nombre = nombre \end{verbatim} \afterverb % Casi para cualquier juego de naipes, es necesario agregar y quitar cartas del mazo. La eliminación de cartas ya ha sido resuelta, pues {\tt Mano} hereda {\tt eliminaCarta} de {\tt Mazo}. Pero deberemos escribir {\tt agregaCarta}: \beforeverb \begin{verbatim} class Mano(Mazo): ... def agregaCarta(self,carta) : self.cartas.append(carta) \end{verbatim} \afterverb % De nuevo, los puntos suspensivos indican que hemos omitido los otrs métodos. El método de lista {\tt append} agrega la nueva carta al final de la lista de cartas. \section{El reparto de los naipes} \index{reparto de naipes} Ahora que ya tenemos la clase {\tt Mano}, queremos repartir las cartas del {\tt Mazo} en manos. No es claramente obvio si este método debe ir en la clase {\tt Mano} o en la clase {\tt Mazo}, pero como opera sobre un mazo único y (posiblemente) sobre varias manos, es más natural ponerlo en el {\tt Mazo}. {\tt repartir} debe ser bastante general, pues los diferentes juegos tienen distintos requerimentos. Puede que necesitemos repartir todo el mazo de una vez, o que agreguemos una carta a cada mano. {\tt 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 método reparte todas las cartas y se detiene: \beforeverb \begin{verbatim} class Mazo : ... def repartir(self, manos, nCartas=999): nManos = len(manos) for i in range(nCartas): if self.estaVacio(): break # fin si se acaban las cartas carta = self.darCarta() # da la carta superior mano = manos[i % nManos] # a quién le toca? mano.agregaCarta(carta) # agrega la carta a la mano \end{verbatim} \afterverb % El segundo parámetro, {\tt nCartas} es opcional; el valor por omisión es un número muy grande, lo cual es lo mismo que decir que se repartirán todos los naipes del mazo. \index{variable de bucle} \index{variable!bucle} La variable de bucle {\tt i} va desde 0 hasta {\tt nCartas-1}. A cada paso a través del bucle, se elimina una carta del mazo mediante el método de lista {\tt pop}, que quita y devuelve el último elemento de la lista. \index{operador módulo} \index{operador!módulo} El operador módulo ({\tt \%}) permite que podamos repartir las cartas de una en una (una carta cada vez para cada mano). Cuando {\tt i} es igual a la cantidad de manos en la lista, la expresión {\tt i \% nManos} salta hacia el comienzo de la lista (el índice es 0). \section {Mostremos la mano} \index{impresión!mano de cartas} Para mostrar el contenido de una mano, podemos sacar partido de la existencia de los métodos {\tt muestraMazo} y {\tt \_\_str\_\_} que se heredan de {\tt Mazo}. Por ejemplo: %\adjustpage{-2} %\pagebreak \beforeverb \begin{verbatim} >>> mazo = Mazo() >>> mazo.mezclar() >>> mano = Mano("hugo") >>> mazo.repartir([mano], 5) >>> print mano La mano de hugo contiene 2 de Picas 3 de Picas 4 de Picas As de Corazones 9 de Tréboles \end{verbatim} \afterverb % No es una gran mano, pero tiene lo necesario como para disponer de una escalera de color. \index{escalera de color} Aunque es conveniente usar la herencia de los métodos existentes, existe información adicional en una {\tt Mano} que desearíamos mostrar al imprimirla. Para ello, podemos proporcionar a la clase {\tt Mano} un método {\tt \_\_str\_\_} que reemplace al de la clase {\tt Mazo}: \beforeverb \begin{verbatim} class Mano(Mazo) ... def __str__(self): s = "La mano de " + self.nombre if self.estaVacio(): s = s + " está vacía\n" else: s = s + " contiene\n" return s + Mazo.__str__(self) \end{verbatim} \afterverb % Al principio {\tt s} es una cadena de caracteres que identifica a la mano. Si la mano está vacía, el programa agrega las palabras {\tt está vacía} y devuelve {\tt s}. En caso contrario, el programa agrega la palabra {\tt contiene} y la representación como cadena de caracteres del {\tt Mazo}, que se obtiene llamando al método {\tt \_\_str\_\_} de la clase {\tt Mazo} sobre la instancia {\tt self}. Puede parecer extraño que enviemos a {\tt self}, que se refiere a la {\tt Mano} actual, como argumento de un método de la clase {\tt Mazo}, hasta que nos damos cuenta de que una {\tt Mano} es un tipo de {\tt Mazo}. Los objetos {\tt Mano} pueden hacer cualquier cosa que pueda hacer un objeto {\tt Mazo}, y por ello es legal que pasemos una {\tt Mano} a un método de {\tt Mazo}. \index{subclase} \index{clase padre} \index{clase!padre} En general, siempre es legal usar una instancia de una subclase en el lugar de una instancia de una clase padre. \section {La clase {\tt JuegoDeCartas}} La clase {\tt JuegoDeCartas} asume la responsabilidad sobre algunas obligaciones básicas comunes a todos los juegos, tales como la creación del mazo y la mezcla de los naipes: \beforeverb \begin{verbatim} class JuegoDeCartas: def __init__(self): self.mazo = Mazo() self.mazo.mezclar() \end{verbatim} \afterverb % Esta es la primera vez que vemos que un método de inicialización realiza una actividad computacional significativa, más allá de la inicialización de atributos. Para implementar juegos específicos, debemos heredar de {\tt JuegoDeCartas} y agregar las características del nuevo juego. Como ejemplo, escribiremos una simulación para La Mona. La meta de La Mona es desembarazarse de las cartas que uno tiene en la mano. Uno se saca las cartas de encima emparejándolas por valor y color. Por ejemplo, el 4 de Tréboles se empareja con el 4 de Picas porque ambos palos son negros. La Sota de Corazones se empareja con la Sota de Diamantes porque ambos son rojos. Para iniciar el juego, se elimina la Reina de Tréboles del mazo, de manera que la Reina de Picas no tiene con quién emparejarse. Las cincuenta y una cartas restantes se reparten entre los jugadores, de una en una. Luego del reparto, todos los jugadores emparejan y descartan tantas cartas como sea posible. Cuando no se pueden realizar más concordancias, el juego comienza. Por turnos, cada jugador toma una carta (sin mirarla) del vecino más cercano de la izquierda que aún tiene cartas. Si la carta elegida concuerda con una de la mano del jugador, se elimina dicho par. Si no, la carta se agrega a la mano del jugador. Llega el momento en el que se realizan todas las concordancias posibles, con lo que queda sólo la Reina de Picas en la mano del perdedor. En nuestra simulación informática del juego, la computadora juega todas las manos. Desafortunadamente, se pierden algunos de los matices del juego real. En una partida real, el jugador que tiene la Mona realiza ciertos esfuerzos para que su vecino la tome, por ejemplo mostrándola prominentemente o al contrario, errando al intentar mostrarla abiertamente, o incluso puede fallar al tratar de errar en su intento de mostrarla prominentemente. La computadora simplemente toma una carta al azar de su vecino. \section {La clase {\tt ManoDeLaMona}} \index{clase!ManoDeLaMona} Una mano para jugar a La Mona requiere ciertas capacidades que están más allá de las que posee una {\tt Mano}. Definiremos una nueva clase {\tt ManoDeLaMona}, que hereda de {\tt Mano} y nos proporciona un método adicional denominado {\tt eliminaCoincidencias}: \beforeverb \begin{verbatim} class ManoDeLaMona(Mano): def eliminaCoincidencias(self): cant = 0 cartasOriginales = self.cartas[:] for carta in cartasOriginales: empareja = Carta(3 - carta.palo, carta.valor) if empareja in self.cartas: self.cartas.remove(carta) self.cartas.remove(empareja) print "Mano %s: %s con %s" % (self.nombre,carta,empareja) cant = cant + 1 return cant \end{verbatim} \afterverb % Comenzamos por hacer una copia de la lista de las cartas, de tal manera que podamos recorrer la copia mientras vamos quitando cartas de la lista original. Como {\tt self.cartas} se modifica en el bucle, no vamos a querer usarla para controlar el recorrido. ¡Python puede quedar realmente confundido si se recorre una lista que está cambiando! \index{recorrido} %\adjustpage{1} Para cada carta de la mano, averiguamos cuál es la carta que concordará con ella y la buscamos. La carta que concuerda tiene el mismo valor y el otro palo del mismo color. La expresión {\tt 3 - carta.palo} transforma un Trébol (palo 0) en una Pica (palo 3) y un Diamante (palo 1) en un Corazón (palo 2). Verifique por su cuenta que las operaciones opuestas también funcionan. Si la carta que concuerda está en la mano, ambas se eliminan. El siguiente ejemplo demuestra el uso de {\tt eliminaCoincidencias}: \beforeverb \begin{verbatim} >>> juego = JuegoDeCartas() >>> mano = ManoDeLaMona("hugo") >>> juego.mazo.repartir([mano], 13) >>> print mano La mano de hugo contiene As de Picas 2 de Diamantes 7 de Picas 8 de Tréboles 6 de Corazones 8 de Picas 7 de Tréboles Raina de Tréboles 7 de Diamantes 5 de Tréboles Sota de Diamantes 10 de Diamantes 10 de Corazones >>> mano.eliminaCoincidencias() Mano hugo: 7 de Picas con 7 de Tréboles Mano hugo: 8 de Picas con 8 de Tréboles Mano hugo: 10 de Diamantes con 10 de Corazones \end{verbatim} \afterverb % Debe usted notar que no existe un método {\tt \_\_init\_\_} para la clase {\tt ManoDeLaMona}. Lo heredamos de {\tt Mano}. \section {La clase {\tt JuegoDeLaMona}} \index{clase!JuegoDeLaMona} Ahora podemos poner nuestra atención en el juego en sí mismo. {\tt JuegoDeLaMona} es una subclase de {\tt JuegoDeCartas} con un método nuevo denominado {\tt jugar} que toma una lista de jugadores como parámetro. Como el método {\tt \_\_init\_\_} se hereda de {\tt JuegoDeCartas}, el nuevo objeto {\tt JuegoDeLaMona} contiene un mazo recientemtente mezclado: %\adjustpage{-1} \beforeverb \begin{verbatim} class JuegoDeLaMona(JuegoDeCartas): def jugar(self, nombres): # quitamos la Reina de Tréboles self.mazo.eliminaCarta(Carta(0,12)) # construimos una mano para cada jugador self.manos = [] for nombre in nombres : self.manos.append(ManoDeLaMona(nombre)) # repartimos los naipes self.mazo.repartir(self.manos) print "----- Se han repartido las cartas." self.muestraManos() # eliminamos las coincidencias iniciales emparejadas = self.eliminaTodasLasCoincidencias() print "----- Coincidencias eliminadas, el juego comienza." self.muestraManos() # se juega hasta que se han descartado las 50 cartas turno = 0 cantManos = len(self.manos) while emparejadas < 25: emparejadas = emparejadas + self.jugarUnTurno(turno) turno = (turno + 1) % cantManos print "----- El juego terminó." self.muestraManos() \end{verbatim} \afterverb % Algunos de los pasos que componen el juego se han colocado en métodos separados. {\tt eliminaTodasLasCoincidencias} recorre la lista de manos y llama a {\tt eliminaCoincidencias} para cada una de ellas: \beforeverb \begin{verbatim} class JuegoDeLaMona(JuegoDeCartas): ... def eliminaTodasLasCoincidencias(self): cant = 0 for mano in self.manos: cant = cant + mano.eliminaCoincidencias() return cant \end{verbatim} \afterverb % \begin{quote} {\em Como ejercicio, escriba {\tt muestraManos}, el cual recorre {\tt self.manos} y muestra cada mano.} \end{quote} {\tt cant} es un acumulador que va sumando la cantidad de concordancias en cada mano y devuelve el total. \index{acumulador} Cuando la cantidad total de coincidencias alcanza a las veinticinco significa que se han eliminado cincuenta cartas de las manos, lo que es lo mismo que decir que sólo queda una carta y el juego ha terminado. La variable {\tt turno} recuerda el turno de cuál jugador se está jugando. Comienza en cero y se incrementa en uno cada vez; cuando alcanza el valor {\tt cantManos}, el operador de módulo lo hace volver a cero. El método {\tt jugarUnTurno} toma un parámetro que indica de quién es el turno. El valor de retorno es la cantidad de concordancias que se han realizado durante ese turno: %\adjustpage{-2} %\pagebreak \beforeverb \begin{verbatim} class JuegoDeLaMona(JuegoDeCartas): ... def jugarUnTurno(self, i): if self.manos[i].estaVacio(): return 0 vecino = self.encuentraVecino(i) cartaElegida = self.manos[vecino].darCarta() self.manos[i].agregaCarta(cartaElegida) print "Mano", self.manos[i].nombre, "eligió", cartaElegida cant = self.manos[i].eliminaCoincidencias() self.manos[i].mezclar() return cant \end{verbatim} \afterverb % Si la mano de un jugador está vacía, el jugador salió del juego, así que no hace nada y devuelve 0. Si no, un turno consiste en encontrar el primer jugador a la izquierda que aún tiene cartas, tomar una carta de las que posee, y controlar si hay concordancias. Antes de volver se mezclan las cartas de la mano, de tal manera que la selección del siguiente jugador sea al azar. El método {\tt encuentraVecino} comienza con el jugador que está inmediatamante a la izquierda y continúa alrededor del círculo hasta que encuentra un jugador que aún tiene cartas. \beforeverb \begin{verbatim} class JuegoDeLaMona(JuegoDeCartas): ... def encuentraVecino(self, i): cantManos = len(self.manos) for proximo in range(1,cantManos): vecino = (i + proximo) % cantManos if not self.manos[vecino].estaVacio(): return vecino \end{verbatim} \afterverb % Si por cualquier motivo {\tt encuentraVecino} llegara a dar la vuelta completa al círculo sin encontrar cartas, devolvería {\tt None} y eso causaría un error en alguna otra parte del programa. Afortunadamante podemos probar que eso no va a suceder nunca (siempre y cuando se detecte correctamente el final del juego). Hemos omitido el método {\tt muestraManos}. Ése puede escribirlo usted mismo. La siguiente salida proviene de una forma reducida del juego, en la cual solamente se reparten las quince cartas más altas (desde los dieces hacia arriba) a tres jugadores. Con este mazo más pequeño, el juego termina tras siete coincidencias, en lugar de veinticinco. \beforeverb \begin{verbatim} >>> import cartas >>> juego = cartas.JuegoDeLaMona() >>> juego.jugar(["Allen","Jeff","Chris"]) ----- Se han repartido las cartas. Mano Allen contiene Rey de Corazones Sota de Tréboles Reina de Picas Rey de Picas 10 de Diamantes Mano Jeff contiene Reina de Corazones Sota de Picas Sota de Corazones Rey de Diamantes Reina de Diamantes Mano Chris contiene Sota de Diamantes Rey de Tréboles 10 de Picas 10 de Corazones 10 de Tréboles Mano Jeff: Reina de Corazones con Reina de Diamantes Mano Chris: 10 de Picas con 10 de Tréboles ----- Se eliminaron las coincidencias, el juego comienza. Mano Allen contiene Rey de Corazones Sota de Tréboles Reina de Picas Rey de Picas 10 de Diamantes Mano Jeff contiene Sota de Picas Sota de Corazones Rey de Diamantes Mano Chris contiene Sota de Diamantes Rey de Tréboles 10 de Corazones Mano Allen: eligió Rey de Diamantes Mano Allen: Rey de Corazones con Rey de Diamantes Mano Jeff: eligió 10 de Corazones Mano Chris: eligió Sota de Tréboles Mano Allen: eligió Sota de Corazones Mano Jeff: eligió Sota de Diamantes Mano Chris: eligió Reina de Picas Mano Allen: eligió Sota de Diamantes Mano Allen: Sota de Corazones con Sota de Diamantes Mano Jeff: eligió Rey de Tréboles Mano Chris: eligió Rey de Picas Mano Allen: eligió 10 de Corazones Mano Allen: 10 de Diamantes con 10 de Corazones Mano Jeff: eligió Reina de Picas Mano Chris: eligió Sota de Picas Mano Chris: Sota de Tréboles con Sota de Picas Mano Jeff: eligió Rey de Picas Mano Jeff: Rey de Tréboles con Rey de Picas ----- El juego terminó. La mano de Allen está vacía. La mano de Jeff contiene Reina de Picas La mano de Chris está vacía. \end{verbatim} \afterverb % Así que Jeff es quien perdió. \section{Glosario} \begin{description} \item[herencia:] La capacidad de definir una nueva clase que es una versión modificada de una clase previamente definida. \item[clase padre:] Aquella clase de la cual la clase hija hereda. \item[clase hija:] Una nueva clase creada heredando de una clase existente; también se la llama ``subclase''. \index{herencia} \index{clase padre} \index{clase hija} \index{subclase} \end{description} %%% Local Variables: %%% mode: latex %%% TeX-master: "top" %%% End:


Next Up Previous Hi Index