Next Up Previous Hi Index

Chapter 16

\selectlanguage{spanish \chapter{Herencia} \section{Herencia} \index{herencia} \index{programación orientada a objetos} \index{clase padre} \index{clase hija} \index{subclase} La característica de un lenguaje que más se asocia con la programación orientada a objetos es la {\bf 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 {\bf padre}. La nueva clase puede denominarse clase {\bf hija}, o también «subclase». \index{diseño orientado a objetos} La herencia es una característica poderosa. Ciertos programas serían complicados sin herencia, pero 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 estructura del problema, lo cual hace que el programa sea más fácil de comprender. Por otro lado, la herencia pude hacer que los porgramas sean más difíciles de leer. Cuando se llama a un método, a veces no es claro dónde debe uno encontrar su definición. El código relevante puede estar diseminado entre varios módulos. Además, muchas de las cosas que se hacen mediante el uso de la herencia, se pueden lograr (más) elegantemente 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 al «Old Maid»\revisar{old maid: que es?}. Una de nuestras metas será que el código que escribamos se pueda reusar para implementar otros juegos de naipes. \section{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. De acuerdo al juego que se esté jugando, podemos querer realizar ciertas operaciones sobre una mano que no tienen sentido sobre un mazo. Por ejemplo, en el \eningles{poker} queremos clasificar una mano (\eningles{straight}, \eningles{flush}, etc.) y compararla con otra. En \eningles{bridge} necesitaremos calcular el puntaje para la mano para así poder hacer una apuesta. 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 nombde 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 en 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, las elipsis 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 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(cartas) for i in range(nCartas): if self.estaVacio(): break # fin si se terminan las cartas carta = self.darCarta() # da la carta superior mano = manos[i % nManos] # ¿quién tiene el siguiente turno? 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 en el 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}, el cual quita y retorna 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 en ronda (una carta a la 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 tomar ventaja de la existencia de los métodos {\tt muestraMazo} y {\tt \_\_str\_\_} que se heredan desde {\tt Mazo}. Por ejemplo: \revisar{salto de página} %\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 un \revisar{straight flush: poker???} \index{straight flush}\revisar{straight flush: poker???} Aunque es conveniente usar la herencia de los métodos existentes, existe información adicional en una mano que desearíamos mostrar. Para ello, podemos proporcionar un método {\tt \_\_str\_\_} en la clase {\tt Mano} 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 % Inicialmente, {\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 de no ser vacía, el programa agrega la palabra {\tt contiene} y la representación como cadena de caracteres del {\tt Mazo}, que se obtiene al llamar al método {\tt \_\_str\_\_} en la clase {\tt Mazo} sobre la instancia {\tt self}.\revisar{en el original no dice instancia, pero queda más claro} 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 Deck}, hasta que nos damos cuenta que una {\tt Mano} es una clase de {\tt Mazo}. Los objetos {\tt Mano} pueden hacer cualquier cosa que puede hacer un objeto {\tt Mazo}, y por ello es legal que pasemos un argumento de clase {\tt Mano} a un método {\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} toma responsabilidad sobre algunas \revisar{chores: en castellano?} básicos 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, fuera de la inicialización de atributos. Para implementar juegos específicos, debemos heredar desde {\tt JuegoDeCartas} y agregar las características para el nuevo juego. Como ejemplo, escribiremos una simulación para el \revisar{Old Maid:????}. La meta del \revisar{Old Maid} es desembarazarse de las cartas que uno tiene en la mano. Uno se saca las cartas de encima apareándolas por valor y color. Por ejemplo, el 4 de Tréboles aparea con el 4 de Picas porque ambos palos son negros. La Sota de Corazones aparea 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 quien aparear. Las cincuenta y un cartas restantes se reparten entre los jugadores, en ronda. Luego del reparto, todos los jugadores aparean y descartan tantas cartas como sea posible. Cuando no se pueden realiazr más concordancias, el juego comienza. Por turnos, cada jugador levanta una carta (sin mirarla) desde el vecino más cercano de la izquierda que aún tiene cartas. Si la carta levantada concuerda con una en la mano del jugador, se elimina dicho par. Sino, la carta se agrega a la mano del jugador. Eventualmente 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 computarizada del juego, la computadora juega todas las manos. Desafortunadamente, algunas de las \revisar{nuances ???} del juego real se pierden. En una partida real, el jugador que tiene a la \revisar{Old Maid} 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 ManoOldMaid}} \index{clase!ManoOldMaid} Una mano para jugar al \revisar{Old Maid} requiere ciertas capacidades que están más allá de las que posee una {\tt Mano}. Definiremos una nueva clase {\tt ManoOldMaid}, que hereda desde {\tt Mano} y proporciona un método adicional denominado {\tt eliminaCoincidencias}: \beforeverb \begin{verbatim} class ManoOldMaid(Mano): def eliminaCoincidencias(self): cant = 0 cartasOriginales = self.cartas[:] for carta in cartasOriginales: aparea = Carta(3 - carta.palo, carta.valor) if aparea in self.cartas: self.cartas.remove(carta) self.cartas.remove(aparea) print "Mano %s: %s concuerda con %s" % (self.nombre,carta,aparea) 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} \revisar{adjustpage??} %\adjustpage{1} Para cada carta en 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). Usted debe verificar 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 eliminaConcordancias}: \beforeverb \begin{verbatim} >>> juego = JuegoDeCartas() >>> mano = ManoOldMaid("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 Jack de Diamantes 10 de Diamantes 10 de Corazones >>> mano.eliminaConcordancias() Mano hugo: 7 de Picas concuerda con 7 de Tréboles Mano hugo: 8 de Picas concuerda con 8 de Tréboles Hand frank: 10 de Diamantes concuerda con 10 de Corazones \end{verbatim} \afterverb % Debe usted notar que no existe un método {\tt \_\_init\_\_} para la clase {\tt ManoOldMaid}. Lo heredamos desde {\tt Mano}. \section {La clase {\tt JuegoOldMaid}} \index{clase!JuegoOldMaid} Ahora podemos poner nuestra atención en el juego en sí mismo. {\tt JuegoOldMaid} es una subclase de {\tt JuegoDeCartas} con nuevo método denominado {\tt jugar} que toma una lista de jugadores como parámetro. Como el método {\tt \_\_init\_\_} se hereda desde {\tt JuegoDeCartas}, el nuevo objeto {\tt JuegoOldMaid} contiene un mazo recientemtente mezclado: \revisar{adjustpage??} %\adjustpage{-1} \beforeverb \begin{verbatim} class JuegoOldMaid(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(ManoOldMaid(nombre)) # repartimos los naipes self.mazo.repartir(self.manos) print "----- Se han repartido las cartas." self.muestraManos() # eliminamos las concordancias iniciales matches = self.eliminaTodasLasConcordancias() print "----- Se eliminaron las concordancias, el juego comienza." self.muestraManos() # se juega hasta que se han descartado las 50 cartas turno = 0 cantManos = len(self.manos) while apareadas < 25: apareadas = apareadas + self.jugarUnTurno(turno) turno = (turno + 1) % cantManos print "----- El juego terminó." self.muestraManos() \end{verbatim} \afterverb % Algunos de los pasos del juego se han puesto en métodos separados. {\tt eliminaTodasLasConcordancias} recorre la lista de manos y llama a {\tt eliminaConcordancias} para cada una de ellas: \beforeverb \begin{verbatim} class JuegoOldMaid(JuegoDeCartas): ... def eliminaTodasLasConcordancias(self): cant = 0 for mano in self.manos: cant = cant + mano.eliminaConcordancias() 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 devuelev el total. \index{acumulador} Cuando la cantidad total de concordancias alcanza a las veinticinco, eso 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: \revisar{adjustpage, pagebreak} %\adjustpage{-2} %\pagebreak \beforeverb \begin{verbatim} class JuegoOldMaid(JuegoDeCartas): ... def jugarUnTurno(self, i): if self.manos[i].estaVacio(): return 0 vecino = self.encuentraVecino(i) cartaLevantada = self.manos[vecino].darCarta() self.manos[i].agregaCarta(cartaLevantada) print "Mano", self.manos[i].nombre, "levantó", cartaLevantada cant = self.manos[i].eliminaConcordancias() 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 ella o él no hace nada y retorna 0. Sino, un turno consta de encontrar el primer jugador a la izquierda que aún tiene cartas, tomar una carta de las que posee, y controlar para ver si hay concordancias. Antes de retornar, las cartas en la mano se mezclan, de tal manera que la selección del siguiente jugador es al azar. El método {\tt encuentraVecino} comienza con el jugador que está inmediatamante a la izquierda y continúa hasta alrededor del círculo hasta que encuentra un jugador que aún tiene cartas. \beforeverb \begin{verbatim} class JuegoOldMaid(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, retornarí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}. Usted puede escribir el suyo propio. La siguiente salida proviene de una forma reducida del juego, en la cual solamente se reparten las quince cartas más altas (desde los diez hacia arriba) a los tres jugadores. Con este mazo más pequeño, el juego termina luego de siete concordancias, en lugar de veinticinco. \beforeverb \begin{verbatim} >>> import cartas >>> juego = cartas.JuegoOldMaid() >>> 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 concuerda con Reina de Diamantes Mano Chris: 10 de Picas concuerda con 10 de Tréboles ----- Se eliminaron las concordancias, 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: levantó Rey de Diamantes Mano Allen: Rey de Corazones concuerda con Rey de Diamantes Mano Jeff: levantó 10 de Corazones Mano Chris: levantó Sota de Tréboles Mano Allen: levantó Sota de Corazones Mano Jeff: levantó Sota de Diamantes Mano Chris: levantó Reina de Picas Mano Allen: levantó Sota de Diamantes Mano Allen: Sota de Corazones concuerda con Sota de Diamantes Mano Jeff: levantó Rey de Tréboles Mano Chris: levantó Rey de Picas Mano Allen: levantó 10 de Corazones Mano Allen: 10 de Diamantes concuerda con 10 de Corazones Mano Jeff: levantó Reina de Picas Mano Chris: levantó Sota de Picas Mano Chris: Sota de Tréboles concuerda con Sota de Picas Mano Jeff: levantó Rey de Picas Mano Jeff: Rey de Tréboles concuerda con Rey de Picas ----- El juego terminó. Mano Allen está vacía. Mano Jeff contiene Reina de Picas Mano 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 desde la cual la clase hija hereda. \item[clase hija:] Una nueva clase creada heredando desde 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