Herramientas Informaticas

Mes: agosto 2012 Página 15 de 22

14.9. Polimorfismo

La mayoría de los métodos que hemos escrito funcionan solo para un tipo específico. Cuando usted crea un nuevo objeto, escribe métodos que operan sobre ese tipo.

Pero hay ciertas operaciones que querrá aplicar a muchos tipos, como las operaciones aritméticas de las secciones anteriores. Si muchos tipos admiten el mismo conjunto de operaciones, puede escribir funciones que trabajen sobre cualquiera de esos tipos.

Por ejemplo, la operación multisuma (común en algebra lineal) toma tres parámetros; multiplica los dos primeros y luego suma el tercero. Podemos escribirla en Python así:

   1: def multisuma (x, y, z):

   2: return x * y + z

Este metodo trabajara con cualquier valor de x e y que se pueda multiplicar y con cualquier valor de z que se pueda sumar al producto.

Podemos invocarlo con valores numéricos:

   1: >>> multisuma (3, 2, 1)

   2: 7

O con Puntos:

   1: >>> p1 = Punto(3, 4)

   2: >>> p2 = Punto(5, 7)

   3: >>> print multisuma (2, p1, p2)

   4: (11, 15)

   5: >>> print multisuma (p1, p2, 1)

   6: 44

 

En el primer caso, el Punto se multiplica por un escalar y luego se suma a otro Punto. En el segundo caso, el producto interior produce un valor numérico, así que el tercer parámetro también debe ser un valor numérico.

Una función como esta que puede tomar parámetros con diferentes tipos se
llama polimórfica.

Como un ejemplo mas, observe el metodo delDerechoYDelReves, que imprime dos veces una lista, hacia adelante y hacia atrás:

   1: def delDerechoYDelReves(derecho):

   2:     import copy

   3:     reves = copy.copy(derecho)

   4:     reves.reverse()

   5:     print str(derecho) + str(reves)

 

Como el metodo reverse es un modificador, hacemos una copia de la lista antes de darle la vuelta. Así, este metodo no modifica la lista que recibe como parámetro.

 

He aquí un ejemplo que aplica delDerechoYDelReves a una lista:

   1: >>> miLista = [1, 2, 3, 4]

   2: >>> delDerechoYDelReves(miLista)

   3: [1, 2, 3, 4][4, 3, 2, 1]

Por supuesto, pretend³amos aplicar esta función a listas, as³ que no es sorprendente que funcione. Lo sorprendente es que pudiéramos usarla con un Punto.

Para determinar si una función se puede aplicar a un nuevo tipo, aplicamos la regla fundamental del polimorfismo:

Si todas las operaciones realizadas dentro de la función se pueden aplicar al tipo, la función se puede aplicar al tipo.

Las operaciones del metodo incluyen copy, reverse y print.copy trabaja sobre cualquier objeto, y ya hemos escrito un metodo str para los Puntos, así que todo lo que necesitamos es un metodo reverse en la clase Punto:

   1: def reverse(self):

   2: self.x , self.y = self.y, self.x

Ahora podemos pasar Puntos a delDerechoYDelReves:

 

   1: >>> p = Punto(3, 4)

   2: >>> delDerechoYDelReves(p)

   3: (3, 4)(4, 3)

El mejor tipo de polimorfismo es el que no se busca, cuando usted descubre que una función que hab³a escrito se puede aplicar a un tipo para el que nunca la había planeado.

 

 

 

14.10. Glosario

lenguaje orientado a objetos: Un lenguaje que ofrece características, como clases definidas por el usuario y herencia, que facilitan la programación orientada a objetos.

programación orientada a objetos: Un estilo de programación en el que los datos y las operaciones que los manipulan están organizadas en clases y métodos.

metodo: Una función definida dentro de una definición de clase y que se invoca sobre instancias de esa clase.

imponer: Reemplazar una opción por omisión. Los ejemplos incluyen el reemplazo de un parámetro por omisión con un argumento particular y el reemplazo de un metodo por omisión proporcionando un nuevo metodo con el
mismo nombre.

metodo de inicialización: Un metodo especial que se invoca automáticamente al crear un nuevo objeto y que inicializa los atributos del objeto.

sobrecarga de operadores: Ampliar los operadores internos (+, -, *, >, <,etc.) de modo que trabajen con tipos definidos por el usuario.

producto interno: Una operación definida en algebra lineal que multiplica dos Puntos y entrega un valor numérico.

multiplicación escalar: Una operación definida en algebra lineal que multiplica cada una de las coordenadas de un Punto por un valor numérico.

polimórfica: Una función que puede operar sobra mas de un tipo. Si todas las operaciones realizadas dentro de una función se pueden aplicar a un tipo, la función se puede aplicar a ese tipo.

Capítulo 15

Conjuntos de objetos

15.1. Composición

Hasta ahora, ya ha visto varios ejemplos de composición. Uno de los primeros ejemplos fue el uso de la llamada a un metodo como parte de una expresión.

Otro ejemplo es la estructura anidada de las sentencias; se puede escribir una sentencia if dentro de un bucle while, dentro de otra sentencia 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 Carta como ejemplo.

15.2. Objetos Carta

Si no esta 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 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.

Si queremos definir un nuevo objeto para representar un naipe, es obvio que atributos debería tener: valor y 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 “Picas” para los palos y “Reina” para los valores. Un problema de esta implementación es que no será fácil comparar naipes para ver cual tiene mayor valor o palo.

Una alternativa es usar números enteros para codificar los valores y palos. Con el termino “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:

Picas 73
Corazones 72
Diamantes 71
Tréboles 7 0

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 ¯guras:

Sota 711
Reina 7 12
Rey 713

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 Carta se parecerá a:

   1: class Carta:

   2:     def __init__(self, palo=0, valor=0):

   3:         self.palo = palo

   4:         self.valor = valor

 

Como acostumbramos, proporcionaremos un metodo de inicialización que toma un parámetro opcional para cada atributo.
Para crear un objeto que representa el 3 de Tréboles, usaremos la instrucción:

   1: tresDeTreboles = Carta(0, 3)

 

El primer argumento, 0, representa el palo de Tréboles.

 

15.3. Atributos de clase y el método __str__

Para poder imprimir los objetos 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 atributos de clase al principio de la definición de clase:

   1: class Carta:

   2:     listaDePalos = ["Tr¶eboles", "Diamantes", "Corazones",

   3:     "Picas"]

   4:     listaDeValores = ["nada", "As", "2", "3", "4", "5", "6", "7",

   5:     "8", "9", "10", "Sota", "Reina", "Rey"]

   6:     # se omite el m¶etodo init

   7:     def __str__(self):

   8:         return (self.listaDeValores[self.valor] + " de " +

   9:                 self.listaDePalos[self.palo])

Un atributo de clase se define fuera de cualquier metodo, y puede accederse desde cualquiera de los métodos de la clase.

Dentro de str , podemos usar listaDePalos y listaDeValores para asociar los valores numéricos de palo y valor con cadenas de caracteres. Por ejemplo, la expresión self.listaDePalos[self.palo] significa usa el atributo palo del objeto self como un índice dentro del atributo de clase denominado listaDePalos, y selecciona la cadena apropiada”.

El motivo del “nada” en el primer elemento de listaDeValores es para relleno del elemento de posición cero en la lista, que nunca se usara. 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:

   1: >>> carta1 = Carta(1, 11)

   2: >>> print carta1

   3: Sota de Diamantes

Los atributos de clase como listaDePalos son compartidos por todos los objetos de tipo Carta. La ventaja de esto es que podemos usar cualquier objeto Carta para acceder a los atributos de clase:

   1: >>> carta2 = Carta(1, 3)

   2: >>> print carta2

   3: 3 de Diamantes

   4: >>> print carta2.listaDePalos[1]

   5: Diamantes

 

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:

   1: >>> carta1.listaDePalos[1] = "Ballenas Bailarinas"

   2: >>> print carta1

   3: Sota de Ballenas Bailarinas

El problema es que todos los Diamantes se transformaran en Ballenas Bailarinas:

   1: >>> print carta2

   2: 3 de Ballenas Bailarinas

En general no es una buena idea modificar los atributos de clase.

15.4. Comparación de naipes

Para los tipos primitivos, existen operadores condicionales (, ==, 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 metodo llamado __cmp__ . Por convención, cmp toma dos parámetros, self y 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.

Algunos tipos están completamente ordenados, lo que significa que se pueden comparar dos elementos cualesquiera y decir cual 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 que 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, >cual es mejor?, >el 3 de Tréboles o el 2 de Diamantes?. Uno tiene mayor valor, pero el otro tiene mayor palo.A los fines de hacer que los naipes sean comparables, se debe decidir que es mas importante: valor o palo.

Para no mentir, la selección es arbitraria. Como algo hay que elegir, diremos que el palo es mas 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 cmp :

   1: def __cmp__(self, otro):

   2:     # controlar el palo

   3:     if self.palo > otro.palo: return 1

   4:     if self.palo < otro.palo: return -1

   5:     # si son del mismo palo, controlar el valor

   6:     if self.valor > otro.valor: return 1

   7:     if self.valor < otro.valor: return -1

   8:     # los valores son iguales, es un empate

   9:     return 0

En este ordenamiento, los Ases son menores que los doses.
Como ejercicio, modifique __cmp__ de tal manera que los Ases tengan mayor valor que los Reyes.

15.5. Mazos de naipes

Ahora que ya tenemos los objetos para representar las Cartas, el próximo paso lógico es definir una clase para representar un Mazo. Por supuesto, un mazo esta compuesto de naipes, as³ que cada objeto Mazo contendrá una lista de
naipes como atributo.

A continuación se muestra una definición para la clase Mazo. El metodo de inicialización crea el atributo cartas y genera el conjunto estándar de cincuenta y dos naipes.

   1: class Mazo:

   2:     def __init__(self):

   3:         self.cartas = []

   4:             for palo in range(4):

   5:                 for valor in range(1, 14):

   6:                     self.cartas.append(Carta(palo, valor))

La forma mas 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 Carta con el palo y valor actual, y agrega dicho naipe a la lista de cartas.

El metodo append funciona sobre listas pero no sobre tuplas, por supuesto.

15.6. Impresión del mazo de naipes

Como es usual, cuando definimos un nuevo tipo de objeto queremos un metodo que imprima el contenido del objeto. Para imprimir un Mazo, recorremos la lista e imprimimos cada Carta:

   1: class Mazo:

   2:     ...

   3:     def muestraMazo(self):

   4:         for carta in self.cartas:

   5:         print carta

 

Desde ahora en adelante, los puntos suspensivos (…) indicaran que hemos omitido los otros métodos en la clase.

En lugar de escribir un metodo muestraMazo, podríamos escribir un metodo __str__ para la clase Mazo. La ventaja de __str__ esta en que es mas flexible. En lugar de imprimir directamente el contenido del objeto, 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 str que retorna una representación como cadena de caracteres de un Mazo. Para darle un toque especial, acomoda los naipes en una cascada, de tal manera que cada naipe esta sangrado un espacio
mas que el precedente.

   1: class Mazo:

   2:     ...

   3:     def __str__(self):

   4:         s = ""

   5:         for i in range(len(self.cartas)):

   6:             s = s + " "*i + str(self.cartas[i]) + "n"

   7:     return s

Este ejemplo demuestra varias características. Primero, en lugar de recorrer self.cartas y asignar cada naipe a una variable, usamos 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 mas que el anterior. La expresión *i proporciona una cantidad de espacios igual al valor actual de i.

Tercero, en lugar de usar la instrucción print para imprimir los naipes, utilizamos la función str. El pasar un objeto como argumento a str es equivalente a invocar el metodo __str__ sobre dicho objeto.

Finalmente, usamos la variable s como acumulador. Inicialmente, 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 s para obtener el nuevo valor. Cuando el bucle termina, s contiene la representación completa en formato de cadena de caracteres del Mazo, la cual se ve como a continuación se presenta:

   1: >>> mazo = Mazo()

   2: >>> print mazo

   3: As de Treboles

   4: 2 de Treboles

   5:  3 de Treboles

   6:   4 de Treboles

   7:    5 de Treboles

   8:     6 de Treboles

   9:      7 de Treboles

  10:       8 de Treboles

  11:        9 de Treboles

  12:         10 de Treboles

  13:           Sota de Treboles

  14:            Reina de Treboles

  15:              Rey de Treboles

  16:                As of Diamantes

Y así sucesivamente. Aun cuando los resultados aparecen en 52 renglones, se trata de solo una única larga cadena de caracteres que contiene los saltos de línea.

15.7. Barajar el mazo

Si un mazo esta 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.

Para mezclar el mazo, utilizaremos la función randrange del modulo random. Esta función toma dos enteros como argumentos a y b, y elige un numero entero en forma aleatoria en el rango a <= x <b. Como el l³mite superior es estrictamente menor a 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:

   1: random.randrange(0, len(self.cartas))

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:

   1: class Mazo:

   2:     ...

   3:     def mezclar(self):

   4:         import random

   5:         nCartas = len(self.cartas)

   6:         for i in range(nCartas):

   7:             j = random.randrange(i, nCartas)

   8:                 self.cartas[i], self.cartas[j] =

   9:                 self.cartas[j], self.cartas[i]

En lugar de presuponer que hay cincuenta y dos naipes en el mazo, obtenemos la longitud real de la lista y la almacenamos en nCartas.

Para cada naipe del mazo, seleccionamos un naipe al azar entre aquellos que no han sido intercambiados aun. Luego intercambiamos el naipe actual (i) con el naipe seleccionado (j). Para intercambiar los naipes usaremos la asignación de tuplas, como se describe en la Sección 9.2:

   1: self.cartas[i], self.cartas[j] = self.cartas[j], self.cartas[i]

 

Como ejercicio, reescriba esta línea de código sin usar una asignación de secuencias.

Página 15 de 22

Creado con WordPress & Tema de Anders Norén