Cesar Systems

Herramientas Informaticas

14.5. Argumentos opcionales

Hemos visto funciones internas que toman un numero variable de argumentos.

Por ejemplo, string.find puede tomar dos, tres o cuatro argumentos.

Es posible escribir funciones definidas por el usuario con listas de argumentos opcionales. Por ejemplo, podemos modernizar nuestra propia versión de encuentra para que haga lo mismo que string.find.

Esta es la versión original de la Sección 7.7:

   1: def encuentra(cad, c):

   2:     indice = 0

   3:         while indice < len(cad):

   4:             if str[indice] == c:

   5:             return indice

   6:         indice = indice + 1

   7:     return –1

 

Esta es la versión aumentada y mejorada:

   1: def encuentra(cad, c, comienzo=0):

   2:     indice = comienzo

   3:     while indice < len(cad):

   4:         if str[indice] == c:

   5:             return indice

   6:         indice = indice + 1

   7:     return –1

 

El tercer parámetro, comienzo, es opcional porque se proporciona un valor por omisión, 0. Si invocamos encuentra solo con dos argumentos, utilizamos el valor por omisión y comenzamos por el principio de la cadena:

   1: >>> encuentra("arriba", "r")

   2: 1

Si le damos un tercer parámetro, anula el predefinido:

   1: >>> encuentra("arriba", "r", 2)

   2: 2

   3: >>> encuentra("arriba", "r", 3)

   4: –1

 

Como ejercicio, a~nada un cuarto parámetro, fin, que especifique donde dejar de buscar.

Cuidado: Este ejercicio tiene truco. El valor por omisión de fin debería ser len(cad), pero eso no funciona. Los valores por omisión se evalúan al definir la función, no al llamarla. Cuando se define encuentra, cad aun no existe, así que no puede averiguar su longitud.

14.6. El método de inicialización

El metodo de inicialización es un metodo especial que se invoca al crear un objeto. El nombre de este metodo es __init__ (dos guiones bajos, seguidos de init y dos guiones bajos mas). Un metodo de inicialización para la clase Hora
es así:

   1: class Hora:

   2:     def __init__(self, horas=0, minutos=0, segundos=0):

   3:         self.horas = horas

   4:         self.minutos = minutos

   5:         self.segundos = segundos

No hay conflicto entre el atributo self.horas y el parámetro horas. la notación de punto especifica a que variable nos referimos.

Cuando invocamos el constructor Hora, los argumentos que damos se pasan a init:

   1: >>> horaActual = Hora(9, 14, 30)

   2: >>> horaActual.imprimeHora()

   3: >>> 9:14:30

Como los parámetros son opcionales, podemos omitirlos:

   1: >>> horaActual = Hora()

   2: >>> horaActual.imprimeHora()

   3: >>> 0:0:0

O dar solo el primer parámetro:

   1: >>> horaActual = Hora (9)

   2: >>> horaActual.imprimeHora()

   3: >>> 9:0:0

O los dos primeros parámetros:

   1: >>> horaActual = Hora (9, 14)

   2: >>> horaActual.imprimeHora()

   3: >>> 9:14:0

Finalmente, podemos dar un subconjunto de los parámetros nombrándolos explícitamente:

   1: >>> horaActual = Hora(segundos = 30, horas = 9)

   2: >>> horaActual.imprimeHora()

   3: >>> 9:0:30

14.7. Revisión de los Puntos

Vamos a reescribir la clase Punto de la Sección 12.1 con un estilo mas orientado a objetos:

   1: class Punto:

   2:     def __init__(self, x=0, y=0):

   3:         self.x = x

   4:         self.y = y

   5:     def __str__(self):

   6:         return '(' + str(self.x) + ', ' + str(self.y) + ')'

 

metodo de inicialización toma los valores de x e y como parámetros opcionales; el valor por omisión de cada parámetro es 0.

El siguiente metodo, __str__ , devuelve una representación en forma de cadena de un objeto Punto. Si una clase ofrece un metodo llamado __str__ , se impone al comportamiento por defecto de la función interna __str__ de Python.

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

   2: >>> str(p)

   3: '(3, 4)'

Imprimir un objeto Punto invoca implícitamente a __str__ sobre el objeto, así que definir __str__ también cambia el comportamiento de print:

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

   2: >>> print p

   3: (3, 4)

Cuando escribimos una nueva clase, casi siempre empezamos escribiendo __init__ , que facilita el instanciar objetos, y __str__ , que casi siempre es útil para la depuración.

 

 

14.8. Sobrecarga de operadores

Algunos lenguajes hacen posible cambiar la definición de los operadores internos cuando se aplican a tipos definidos por el usuario. Esta característica se llama sobrecarga de operadores. Es especialmente útil cuando definimos nuevos
tipos matemáticos.

Por ejemplo, para suplantar al operador de suma + necesitamos proporcionar un metodo llamado __add__ :

   1: class Punto:

   2:     # aquí van los métodos que ya habíamos definido...

   3:     def __add__(self, otro):

   4:         return Punto(self.x + otro.x, self.y + otro.y)

Como es habitual, el primer parámetro es el objeto sobre el que se invoca el metodo. El segundo parámetro se llama convenientemente otro para distinguirlo del mismo (self). Para sumar dos Puntos, creamos y devolvemos un nuevo
Punto que contiene la suma de las coordenadas x y la suma de las coordenadas y.

Ahora, cuando apliquemos el operador + a objetos Punto, Python invocara a  __add__ :

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

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

   3: >>> p3 = p1 + p2

   4: >>> print p3

   5: (8, 11)

La expresión p1 + p2 equivale a p1. add (p2), pero es obviamente más elegante.

Como ejercicio, añada un método sub (self, otro) que sobrecargue el operador resta y pruébelo.

   1: def __mul__(self, otro):

   2:     return self.x * otro.x + self.y * otro.y

Si el operando a la izquierda de * es un tipo primitivo y el operando de la derecha es un Punto, Python invoca a __rmul__ , lo que realiza una multiplicación escalar:

   1: def __rmul__(self, otro):

   2:     return Punto(otro * self.x, otro * self.y)

El resultado es un nuevo Punto cuyas coordenadas son múltiplos de las coordenadas originales. Si otro es un tipo que no se puede multiplicar por un numero en coma flotante, entonces rmul causara un error.

Este ejemplo muestra ambos tipos de multiplicación:

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

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

   3: >>> print p1 * p2

   4: 43

   5: >>> print 2 * p2

   6: (10, 14)

Que ocurre si intentamos evaluar p2 * 2? Como el primer parámetro es un Punto, Python invoca a mul con 2 como el segundo parámetro. Dentro de __mul__ , el programa intenta acceder a la coordenada x de otro, pero no lo consigue porque un entero no tiene atributos:

   1: >>> print p2 * 2

   2: AttributeError: 'int' object has no attribute 'x'

Desgraciadamente, el mensaje de error es un poco opaco. Este ejemplo muestra algunas de las dificultades de la programación orientada a objetos. A veces es difícil averiguar simplemente que código se esta ejecutando.

Para ver un ejemplo más completo de sobrecarga de operadores, vaya al Apéndice B.

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.

Página 121 de 143

Creado con WordPress & Tema de Anders Norén