Next Up Previous Hi Index

Chapter 15

\newcommand{\eningles[1]{{\foreignlanguage{english}{#1}}} \newcommand{\revisar}[1]{\textbf{***}\marginpar{\raggedright\tiny\textsc{#1}}} \chapter{Conjuntos de objetos} \section{Composición} \index{composición} \index{estructura anidada} Hasta ahora, ya ha visto varios ejemplos de composición. Uno de los primeros ejemplos fue el uso de la llamada a un método como parte de una expresión. Otro ejemplo es la estructura anidada de las sentencias; se puede escribir una sentencia {\tt if} dentro de un bucle {\tt while}, dentro de otra sentencia {\tt if}, y así sucesivamente. Una vez visto este patrón, y sabiendo acerca de listas y objetos, no le debería sorprender que pueda crear listas de objetos. También puede crear objetos que contengan listas (en forma de atributos); puede crear listas que contengan listas; objetos que contengan objetos, y así indefinidamente. En este capítulo y el siguiente, exploraremos algunos ejemplos de estas combinaciones, y usaremos objetos {\tt Carta} como ejemplo. \section{Objetos {\tt Carta}} \index{Carta} \index{clase!Carta} Si no está usted familiarizado con los naipes de juego comunes, puede ser un buen momento para que consiga un mazo, si no este capítulo puede que no tenga mucho sentido. Hay cincuenta y dos naipes en una baraja inglesa, cada uno de los cuales pertenece a un palo y tiene un valor; hay cuatro palos diferentes y trece valores. Los palos son Picas, Corazones, Diamantes, y Tréboles (en el orden descendente según el \eningles{bridge}). Los valores son As, 2, 3, 4, 5, 6, 7, 8, 9, 10, Sota, Reina, y Rey. Dependiendo del tipo de juego que se juegue, el valor del As puede ser mayor al Rey o inferior al 2. \index{valor} \index{palo} Si queremos definir un nuevo objeto para representar un naipe, es obvio qué atributos debería tener: {\tt valor} y {\tt palo}. Lo que no es tan obvio es el tipo que se debe dar a los atributos. Una posibilidad es usar cadenas de caracteres que contengan palabras como \verb+"Picas"+ para los palos y \verb+"Reina"+ para los valores. Un problema de esta implementación es que no será fácil comparar naipes para ver cuál tiene mayor valor o palo. \index{codificar} \index{cifrar} \index{corresponder} Una alternativa es usar números enteros para {\bf codificar} los valores y palos. Con el término ``codificar'' no queremos significar lo que algunas personas pueden pensar, acerca de cifrar o traducir a un código secreto. Lo que un programador entiende por ``codificar'' es ``definir una correspondencia entre una secuencia de números y los elementos que se desea representar''. Por ejemplo: \beforefig \begin{tabular}{l c l} Picas & $\mapsto$ & 3 \\ Corazones & $\mapsto$ & 2 \\ Diamantes & $\mapsto$ & 1 \\ Tréboles & $\mapsto$ & 0 \end{tabular} \afterfig Esta correspondencia tiene una característica obvia: los palos corresponden a números enteros en orden, o sea que podemos comparar los palos al comparar los números. La asociación de los valores es bastante obvia; cada uno de los valores numéricos se asocia con el entero correspondiente, y para las figuras: \beforefig \begin{tabular}{l c l} Sota & $\mapsto$ & 11 \\ Reina & $\mapsto$ & 12 \\ Rey & $\mapsto$ & 13 \\ \end{tabular} \afterfig Estamos usando una notación matemática para estas asociaciones por una razón: no son parte del programa Python. Son parte del diseño del programa, pero nunca aparecen explícitamente en el código fuente. La definición de clase para el tipo {\tt Carta} se parecerá a: \beforeverb \begin{verbatim} class Carta: def __init__(self, palo=0, valor=0): self.palo = palo self.valor = valor \end{verbatim} \afterverb % Como acostumbramos, proporcionaremos un método de inicialización que toma un parámetro opcional para cada atributo. \index{constructor} Para crear un objeto que representa el 3 de Tréboles, usaremos la instrucción: \beforeverb \begin{verbatim} tresDeTreboles = Carta(0, 3) \end{verbatim} \afterverb % El primer argumento, {\tt 0}, representa el palo de Tréboles. \section{Atributos de clase y el método {\tt \_\_str\_\_}} \index{atributo de clase} \index{atributo!clase} Para poder imprimir los objetos {\tt Carta} de una manera fácil de leer para las personas, vamos a establecer una correspondencia entre los códigos enteros y las palabras. Una manera natural de hacer esto es con listas de cadenas de caracteres. Asignaremos estas listas dentro de {\bf atributos de clase} al principio de la definición de clase: \beforeverb \begin{verbatim} class Carta: listaDePalos = ["Tréboles", "Diamantes", "Corazones", "Picas"] listaDeValores = ["nada", "As", "2", "3", "4", "5", "6", "7", "8", "9", "10", "Sota", "Reina", "Rey"] # se omite el método init def __str__(self): return (self.listaDeValores[self.valor] + " de " + self.listaDePalos[self.palo]) \end{verbatim} \afterverb % Un atributo de clase se define fuera de cualquier método, y puede accederse desde cualquiera de los métodos de la clase. Dentro de {\tt \_\_str\_\_}, podemos usar {\tt listaDePalos} y {\tt listaDeValores} para asociar los valores numéricos de {\tt palo} y {\tt valor} con cadenas de caracteres. Por ejemplo, la expresión \verb+self.listaDePalos[self.palo]+ significa ``usa el atributo {\tt palo} del objeto {\tt self} como un índice dentro del atributo de clase denominado {\tt listaDePalos}, y selecciona la cadena apropiada''. El motivo del {\tt ``nada''} en el primer elemento de {\tt listaDeValores} es para relleno del elemento de posición cero en la lista, que nunca se usará. Los únicos valores lícitos para el valor van de 1 a 13. No es obligatorio que desperdiciemos este primer elemento. Podríamos haber comenzado en 0 como es usual, pero es menos confuso si el 2 se codifica como 2, el 3 como 3, y así sucesivamente. Con los métodos que tenemos hasta ahora, podemos crear e imprimir naipes: \beforeverb \begin{verbatim} >>> carta1 = Carta(1, 11) >>> print carta1 Sota de Diamantes \end{verbatim} \afterverb % Los atributos de clase como {\tt listaDePalos} son compartidos por todos los objetos de tipo {\tt Carta}. La ventaja de esto es que podemos usar cualquier objeto {\tt Carta} para acceder a los atributos de clase: \beforeverb \begin{verbatim} >>> carta2 = Carta(1, 3) >>> print carta2 3 de Diamantes >>> print carta2.listaDePalos[1] Diamantes \end{verbatim} \afterverb % La desventaja es que si modificamos un atributo de clase, afectaremos a cada instancia de la clase. Por ejemplo, si decidimos que ``Sota de Diamantes'' en realidad debería llamarse ``Sota de Ballenas Bailarinas'', podríamos hacer lo siguiente: \index{instancia!objeto} \index{instancia de objeto} \beforeverb \begin{verbatim} >>> carta1.listaDePalos[1] = "Ballenas Bailarinas" >>> print carta1 Sota de Ballenas Bailarinas \end{verbatim} \afterverb % El problema es que \emph{todos} los Diamantes se transformarán en Ballenas Bailarinas: \beforeverb \begin{verbatim} >>> print carta2 3 de Ballenas Bailarinas \end{verbatim} \afterverb % En general no es una buena idea modificar los atributos de clase. \section{Comparación de naipes} \label{comparecard} \index{operador!condicional} \index{operador condicional} Para los tipos primitivos, existen operadores condicionales ({\tt <{} }, {\tt >{}}, {\tt ==}, etc.) que comparan valores y determinan cuando uno es mayor, menor, o igual a otro. Para los tipos definidos por el usuario, podemos sustituir el comportamiento de los operadores internos si proporcionamos un método llamado {\tt \_\_cmp\_\_}. Por convención, {\tt \_\_cmp\_\_} toma dos parámetros, {\tt self} y {\tt otro}, y retorna 1 si el primer objeto es el mayor, -1 si el segundo objeto es el mayor, y 0 si ambos son iguales. \index{sustituir} \index{sobrecarga de operadores} \index{orden} \index{orden completo} \index{orden parcial} Algunos tipos están completamente ordenados, lo que significa que se pueden comparar dos elementos cualesquiera y decir cuál es el mayor. Por ejemplo, los números enteros y los números en coma flotante tienen un orden completo. Algunos conjuntos no tienen orden, o sea, que no existe ninguna manera significativa de decir que un elemento es mayor a otro. Por ejemplo, las frutas no tienen orden, lo que explica por qué no se pueden comparar peras con manzanas. El conjunto de los naipes tiene un orden parcial, lo que significa que algunas veces se pueden comparar los naipes, y otras veces no. Por ejemplo, usted sabe que el 3 de Tréboles es mayor que el 2 de Tréboles y el 3 de Diamantes es mayor que el 3 de Tréboles. Pero, ¿cuál es mejor?, ¿el 3 de Tréboles o el 2 de Diamantes?. Uno tiene mayor valor, pero el otro tiene mayor palo. \index{comparable} A los fines de hacer que los naipes sean comparables, se debe decidir qué es más importante: valor o palo. Para no mentir, la selección es arbitraria. Como algo hay que elegir, diremos que el palo es más importante, porque un mazo nuevo viene ordenado con todos los Tréboles primero, luego con todos los Diamantes, y así sucesivamente. Con esa decisión tomada, podemos escribir {\tt \_\_cmp\_\_}: \beforeverb \begin{verbatim} def __cmp__(self, otro): # controlar el palo if self.palo > otro.palo: return 1 if self.palo < otro.palo: return -1 # si son del mismo palo, controlar el valor if self.valor > otro.valor: return 1 if self.valor < otro.valor: return -1 # los valores son iguales, es un empate return 0 \end{verbatim} \afterverb % En este ordenamiento, los Ases son menores que los doses. \begin{quote} {\em Como ejercicio, modifique {\tt \_\_cmp\_\_} de tal manera que los Ases tengan mayor valor que los Reyes.} \end{quote} \section{Mazos de naipes} \index{lista!de objetos} \index{objetos!lista de} \index{mazo} Ahora que ya tenemos los objetos para representar las {\tt Carta}s, el próximo paso lógico es definir una clase para representar un {\tt Mazo}. Por supuesto, un mazo está compuesto de naipes, así que cada objeto {\tt Mazo} contendrá una lista de naipes como atributo. \index{método de inicialización} \index{método!inicialización} A continuación se muestra una definición para la clase {\tt Mazo}. El método de inicialización crea el atributo {\tt cartas} y genera el conjunto estándar de cincuenta y dos naipes. \index{composición} \index{bucle!anidado} \beforeverb \begin{verbatim} class Mazo: def __init__(self): self.cartas = [] for palo in range(4): for valor in range(1, 14): self.cartas.append(Carta(palo, valor)) \end{verbatim} \afterverb % La forma más fácil de poblar el mazo es mediante un bucle anidado. El bucle exterior enumera los palos desde 0 hasta 3. El bucle interior enumera los valores desde 1 hasta 13. Como el bucle exterior itera cuatro veces, y el interior itera trece veces, la cantidad total de veces que se ejecuta el cuerpo interior es cincuenta y dos (trece por cuatro). Cada iteración crea una nueva instancia de {\tt Carta} con el palo y valor actual, y agrega dicho naipe a la lista de {\tt cartas}. El método {\tt append} funciona sobre listas pero no sobre tuplas, por supuesto. \index{método append} \index{métodos de lista} \index{método!lista} \adjustpage{1} \section{Impresión del mazo de naipes} \label{muestraMazo} \index{impresión!objeto mazo} Como es usual, cuando definimos un nuevo tipo de objeto queremos un método que imprima el contenido del objeto. Para imprimir un {\tt Mazo}, recorremos la lista e imprimimos cada {\tt Carta}: \beforeverb \begin{verbatim} class Mazo: ... def muestraMazo(self): for carta in self.cartas: print carta \end{verbatim} \afterverb % Desde ahora en adelante, los puntos suspensivos ({\tt ...}) indicarán que hemos omitido los otros métodos en la clase. En lugar de escribir un método {\tt muestraMazo}, podríamos escribir un método {\tt \_\_str\_\_} para la clase {\tt Mazo}. La ventaja de {\tt \_\_str\_\_} está en que es más flexible. En lugar de imprimir directamente el contenido del objeto, {\tt \_\_str\_\_} genera una representación en forma de cadena de caracteres que las otras partes del programa pueden manipular antes de imprimir o almacenar para un uso posterior. Se presenta ahora una versión de {\tt \_\_str\_\_} que retorna una representación como cadena de caracteres de un {\tt Mazo}. Para darle un toque especial, acomoda los naipes en una cascada, de tal manera que cada naipe está sangrado un espacio más que el precedente. \beforeverb \begin{verbatim} class Mazo: ... def __str__(self): s = "" for i in range(len(self.cartas)): s = s + " "*i + str(self.cartas[i]) + "\n" return s \end{verbatim} \afterverb % Este ejemplo demuestra varias características. Primero, en lugar de recorrer {\tt self.cartas} y asignar cada naipe a una variable, usamos {\tt i} como variable de bucle e índice de la lista de naipes. Segundo, utilizamos el operador de multiplicación de cadenas de caracteres para sangrar cada naipe un espacio más que el anterior. La expresión {\tt " "*i} prooprciona una cantidad de espacios igual al valor actual de {\tt i}. Tercero, en lugar de usar la instrucción {\tt print} para imprimir los naipes, utilizamos la función {\tt str}. El pasar un objeto como argumento a {\tt str} es equivalente a invocar el método {\tt \_\_str\_\_} sobre dicho objeto. \index{acumulador} Finalmente, usamos la variable {\tt s} como {\bf acumulador}. Inicialmente, {\tt s} es una cadena de caracteres vacía. En cada pasada a través del bucle, se genera una nueva cadena de caracteres que se concatena con el viejo valor de {\tt s} para obtener el nuevo valor. Cuando el bucle termina, {\tt s} contiene la representación completa en formato de cadena de caracteres del {\tt Mazo}, la cual se ve como a continuación se presenta: \adjustpage{-2} \beforeverb \begin{verbatim} >>> mazo = Mazo() >>> print mazo As de Tréboles 2 de Tréboles 3 de Tréboles 4 de Tréboles 5 de Tréboles 6 de Tréboles 7 de Tréboles 8 de Tréboles 9 de Tréboles 10 de Tréboles Sota de Tréboles Reina de Tréboles Rey de Tréboles As of Diamantes \end{verbatim} \afterverb % Y así sucesivamente. Aún cuando los resultados aparecen en 52 renglones, se trata de sólo una única larga cadena de caracteres que contiene los saltos de línea. \section{Barajar el mazo} \index{barajar} Si un mazo está perfectamente barajado, cualquier naipe tiene la misma probabilidad de aparecer en cualquier posición del mazo, y cualquier lugar en el mazo tiene la misma probabilidad de contener cualquier naipe. \index{aleatorio} \index{random} \index{randrange} Para mezclar el mazo, utilizaremos la función {\tt randrange} del módulo {\tt random}. Esta función toma dos enteros como argumentos {\tt a} y {\tt b}, y elige un número entero en forma aleatoria en el rango {\tt a <= x < b}. Como el límite superior es estrictamente menor a {\tt b}, podemos usar la longitud de la lista como el segundo argumento y de esa manera tendremos garantizado un índice legal dentro de la lista. Por ejemplo, esta expresión selecciona el índice de un naipe al azar dentro del mazo: \beforeverb \begin{verbatim} random.randrange(0, len(self.cartas)) \end{verbatim} \afterverb % Una manera sencilla de mezclar el mazo es recorrer los naipes e intercambiar cada una con otra elegida al azar. Es posible que el naipe se intercambie consigo mismo, pero no es un problema. De hecho, si eliminamos esa posibilidad, el orden de los naipes no será completamente al azar: \adjustpage{-2} \beforeverb \begin{verbatim} class Mazo: ... def mezclar(self): import random nCartas = len(self.cartas) for i in range(nCartas): j = random.randrange(i, nCartas) self.cartas[i], self.cartas[j] =\ self.cartas[j], self.cartas[i] \end{verbatim} \afterverb % En lugar de presuponer que hay cincuenta y dos naipes en el mazo, obtenemos la longitud real de la lista y la almacenamos en {\tt nCartas}. \index{intercambio} \index{asignación de tuplas} \index{asignación!tupla} Para cada naipe del mazo, seleccionamos un naipe al azar entre aquellos que no han sido intercambiados aún. Luego intercambiamos el naipe actual ({\tt i}) con el naipe seleccionado ({\tt j}). Para intercambiar los naipes usaremos la asignación de tuplas, como se describe en la Sección~\ref{tuple assignment}: \beforeverb \begin{verbatim} self.cartas[i], self.cartas[j] = self.cartas[j], self.cartas[i] \end{verbatim} \afterverb % \begin{quote} {\em Como ejercicio, reescriba esta línea de código sin usar una asignación de secuencias.} \end{quote} \section{Eliminación y reparto de los naipes} \index{eliminación de naipes} Otro método que podría ser útil para la clase {\tt Mazo} es {\tt eliminaCarta}, que toma un naipe como parámetro, lo elimina, y retorna verdadero (1) si el naipe estaba en el mazo, y falso (0) si no estaba: \beforeverb \begin{verbatim} class Mazo: ... def eliminaCarta(self, carta): if carta in self.cartas: self.cartas.remove(carta) return 1 else: return 0 \end{verbatim} \afterverb % El operador {\tt in} retorna verdadero si el primer operando está en el segundo, el cual debe ser una lista o tupla. Si el primer operando es un objeto, Python usa el método {\tt \_\_cmp\_\_} del objeto para determinar la igualdad entre los elementos de la lista. Como el {\tt \_\_cmp\_\_} en la clase {\tt Carta} verifica la igualdad en profundidad, el método {\tt eliminaCarta} también verifica igualdad en profundidad. \index{operador in} \index{operador!in} Para repartir los naipes, queremos eliminar y devolver el naipe que ocupa la posición superior en el mazo. El método {\tt pop} de las listas proporciona una manera conveniente de realizar esto: \beforeverb \begin{verbatim} class Mazo: ... def darCarta(self): return self.cartas.pop() \end{verbatim} \afterverb % En realidad, {\tt pop} elimina el \emph{último} naipe en la lista, así que en efecto estamos repartiendo desde el extremo inferior del mazo. \index{función booleana} \index{función!booleana} Otra operación más que es muy probable necesitemos es la función booleana {\tt estaVacio}, la cual devuelve verdadero si el mazo no contiene ningún naipe: \beforeverb \begin{verbatim} class Deck: ... def estaVacio(self): return (len(self.cartas) == 0) \end{verbatim} \afterverb \section{Glosario} \begin{description} \item[codificar:] Representar un conjunto de valores uilizando otro conjunto de valores, entre los cuales se construye una correspondencia. \item[atributo de clase:] Una variable que se define dentro de la definición de una clase pero fuera de cualquiera de sus métodos. Los atributos de clase son accesibles desde cualquier método de la clase y están compartidos por todas las instancias de la misma. \item[acumulador:] Una variable que se usa en un bucle para acumular una serie de valores, por ejemplo concatenándolos dentro de una cadena de caracteres o adicionándolos a una suma. \index{codificar} \index{atributo de clase} \index{atributo!clase} \index{acumulador} \end{description} %\selectlanguage{english} %%% Local Variables: %%% mode: latex %%% TeX-master: "top" %%% End:


Next Up Previous Hi Index