![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Python es un lenguaje de programación orientado a objetos, lo que significa que proporciona características que admiten la programación orientada a objetos.
No es fácil definir la programación orientada a objetos, pero ya hemos visto algunas de sus características:
Por ejemplo, la clase Tiempo, que se define en el Capítulo 13, corresponde a nuestra forma de registrar la hora del día, y las funciones que hemos definido corresponden al tipo de cosas que hacemos con dichas horas. De igual forma, las clases Punto y Rectángulo corresponden a los conceptos matemáticos del punto y el rectángulo.
Hasta el momento, no le hemos sacado partido a las características que Python proporciona para admitir la programación orientada a objetos. Lo cierto es que estas características no son necesarias. Para la mayor parte, se proporciona una sintaxis alternativa para lo que ya hemos hecho, pero, en muchos casos, la alternativa es más concisa y transmite de forma más precisa la estructura del programa.
Por ejemplo, en el programa Tiempo no existe una conexión clara entre la definición de clase y las definiciones de función que se encuentran a continuación. Si observamos detenidamente, se puede apreciar que cada función tiene por lo menos un objeto Tiempo como parámetro.
Y es esto precisamente lo que justifica la existencia de los métodos. Ya hemos visto algunos métodos como claves y valores, que se invocaban en los diccionarios. Cada método se encuentra asociado a una clase y se pretende que se invoque a petición de esa clase.
Los métodos son como las funciones, pero con dos diferencias:
En las próximas secciones, utilizaremos las funciones de los dos últimos capítulos y las transformaremos en métodos. Esta transformación es puramente mecánica, se hace simplemente siguiendo una serie de pasos. Si te sientes cómodo convirtiendo una forma en otra, podrás elegir la más adecuada para todo lo que estés haciendo.
Comentarios
En el Capítulo 13, definimos una clase que se llamaba Tiempo y escribiste una función llamada printTiempo parecida a esta:
class Tiempo:
pass
def printTiempo(tiempo):
print str(tiempo.horas) + ":" +
str(tiempo.minutos) + ":" +
str(tiempo.segundos)
Para llamar a esta función, pasamos un objeto Tiempo como parámetro:
>>> Tiempoactual = Tiempo()
>>> Tiempo.horas = 9
>>> Tiempoactual.minutos = 14
>>> Tiempoactual.segundos = 30
>>> printTiempo(Tiempoactual)
Para convertir printTiempo en un método, lo que tenemos que hacer es mover la definición de función dentro de la definición de clase. Fíjate en el cambio en la sangría.
class Tiempo:
def printTiempo(tiempo):
print str(tiempo.horas) + ":" +
str(tiempo.minutos) + ":" +
str(tiempo.minutos) + ":"
Ahora podemos invocar printTiempo utilizando notación de punto.
>>> Tiempoactual.printTiempo()
Como suele ocurrir, el objeto en el que se invoca el método aparece antes del punto, mientras que el nombre del método aparece después.
El objeto con el que se invoca el método se asigna al primer parámetro, por lo tanto, en este caso Tiempoactual se asigna al parámetro tiempo.
Por convención, el primer parámetro de un método se denomina self. Sería un poco complicado explicar el motivo, pero estaría basado en una metáfora muy práctica.
La sintaxis de una llamada de función, printTiempo(Tiempoactual), indica que la función es el agente activo. Diría algo así como, "¡Oye, printTiempo! Aquí tienes que escribir un objeto"
En la programación orientada a objetos, los objetos son los agentes activos. Una invocación como Tiempoactual.printTiempo() diría "¡Oye, Tiempoactual! ¡Escríbelo tú, por favor!"
Este cambio de perspectiva puede ser más educado, lo que no significa que sea útil. De hecho, en los ejemplos que hemos visto hasta ahora, puede que no lo sea. Pero, a veces, el hecho de cambiar el peso de la responsabilidad de las funciones a los objetos hace posible que se puedan escribir más funciones versátiles y simplifica que se mantenga y se reutilice el código.
Comentarios
Convirtamos incremento (de la Sección 13.3) en un método. Para ahorrar espacio, omitiremos los métodos definidos previamente, pero debes guardarlos en la versión:
class Tiempo:
#métodos definidos previamente aquí...
def incremento(self, segundos):
self.segundos = segundos + self.segundos
while self.segundos >= 60:
self.segundos = self.segundos - 60
self.minutos = self.minutos + 1
while self.minutos >= 60:
self.minutos = self.minutos - 60
self.horas = self.horas + 1
La transformación es puramente mecánica, introducimos la definición del método en la definición de clase y cambiamos el nombre del primer parámetro.
Ahora podemos invocar incremento como un método.
Tiempoactual.incremento(500)
De nuevo, el objeto con el que se invoca el método se asigna al primer parámetro, self. El segundo parámetro, segundos obtiene el valor 500.
Como ejercicio, convierte convertirEnSegundos (de la Sección 13.5) en un método en la clase Tiempo.Comentarios
La función despues es un poco más complicada porque opera en dos objetos Tiempo, no en uno. Solo se puede convertir uno de los parámetros en self; el otro se queda igual:
class Tiempo:
#métodos definidos previamente aquí...
def despues(self, tiempo2):
if self.hora > tiempo2.hora:
return 1
if self.hora < tiempo2.hora:
return 0
if self.minuto > tiempo2.minuto:
return 1
if self.minuto < tiempo2.minuto:
return 0
if self.segundo > tiempo2.segundo:
return 1
return 0
Se invoca este método en un objeto y se pasa el otro como argumento:
if Tiemporealizado.despues(Tiempoactual):
print "El pan se elaborará después de que empiece".
La invocación se lee casi como en inglés: "Si el tiempo-realizado está después del tiempo-actual, entonces..."
Comentarios
Hemos visto funciones integradas que toman un número 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 argumento opcional. Por ejemplo, podemos actualizar nuestra propia versión de find para hacer lo mismo que con string.find.
Esta sería la versión original de la Sección 7.7:
def hallar(str, ch):
indice = 0
while indice < len(str):
if str[indice] == ch:
return indice
indice = indice + 1
return -1
Esta es la nueva versión mejorada:
def hallar(str, ch, comienzo=0):
indice = comienzo
while indice < len(str):
if str[indice] == ch:
return indice
indice = indice + 1
return -1
El tercer parámetro, comienzo, es opcional porque se proporciona un valor omitido, 0. Si se invoca hallar con solo dos argumentos, se utiliza el valor omitido y se comienza desde el principio de la cadena.
>>> hallar("arroz", "r")
1
Si se proporciona un tercer parámetro, se impone la omisión:
>>> hallar("arroz", "r", 2)
2
>>> hallar("arroz", "r", 3)
-1
Como ejercicio añade un cuarto parámetro, final, que especifique dónde parar de mirar.ComentariosAviso: Este ejercicio es un poco complicado. El valor omitido de final debería ser len(str), pero no funciona. Los valores omitidos se evalúan cuando se define la función, no cuando se llama. Cuando se define encontrar, str no existe, por lo tanto, no se puede encontrar su longitud.
El método de inicialización es un método especial que se invoca cuando se crea un objeto. El nombre del método se denomina __init__ (dos guiones bajos, seguidos por init, y luego dos caracteres más). Un método de inicialización para la clase Tiempo quedaría de la siguiente forma:
class Tiempo:
def __init__(self, horas=0, minutos=0, segundos=0):
self.horas = horas
self.minutos = minutos
self.segundos = segundos
No existe incompatibilidad entre el atributo self.horas y el parámetro horas. La notación de punto especifica a qué variable nos referimos.
Cuando invocamos al constructor Tiempo los argumentos que proporcionamos se transfieren al init:
>>> Tiempoactual = Tiempo(9, 14, 30)
>>> Tiempoactual.printTiempo()
>>> 9:14:30
Podemos omitir los parámetros, ya que son opcionales:
>>> Tiempoactual = Tiempo()
>>> Tiempoactual.printTiempo()
>>> 0:0:0
O proporcionar solo el primer parámetro:
>>> Tiempoactual = Tiempo(9)
>>> Tiempoactual.printTiempo()
>>> 9:0:0
O los dos primeros parámetros:
>>> Tiempoactual = Tiempo(9, 14)
>>> Tiempoactual.printTiempo()
>>> 9:14:0
Por último, podemos proporcionar un subconjunto de los parámetros nombrándolos explícitamente:
>>> Tiempoactual = Tiempo(segundos = 30, horas = 9)
>>> Tiempoactual.printTiempo()
>>> 9:0:30
Volvamos a escribir la clase Punto de la Sección 12.1 en un estilo más orientado a objetos:
class Punto:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __str__(self):
return '(' + str(self.x) + ', ' + str(self.y) + ')'
El método de inicialización toma los valores x e y como parámetros opcionales; la omisión de cualquier parámetro equivale a 0.
El método siguiente, __str__, devuelve una representación de tipo cadena de un objeto Punto. Si una clase proporciona un método llamado __str__, se impone al funcionamiento predeterminado de la función integrada str de Python.
>>> p = Punto(3, 4)
>>> str(p)
'(3, 4)'
Al mostrar en pantalla un objeto Punto se invoca implícitamente a __str__ en el objeto, por lo tanto, al definir __str__ también cambia el funcionamiento de print:
>>> p = Punto(3, 4)
>>> print p
(3, 4)
Cuando escribimos una nueva clase, casi siempre comenzamos escribiendo __init__, lo que facilita la instanciación de objetos y __str__, que suele ser útil para la depuración.
Comentarios
Algunos lenguajes posibilitan el cambio de definición de los operadores integrados cuando se aplican en tipos definidos por el usuario. Esta característica se denomina sobrecarga de operadores. Es muy útil cuando se definen nuevos tipos matemáticos.
Por ejemplo, para imponer el operador de suma +, proporcionamos un método llamado __add__:
class Punto:
# métodos definidos previamente aquí...
def __add__(self, otro):
return Punto(self.x + otro.x, self.y + otro.y)
Como suele ocurrir, el primer parámetro es el objeto sobre el que se invoca el método. El segundo parámetro se denomina otro para distinguirlo de self. Para sumar dos Puntos, se crea y se devuelve un nuevo Punto que contenga la suma de las coordenadas x y la suma de las coordenadas y.
Entonces, cuando aplicamos el operador + a objetos Punto, Python invoca __add__:
>>> p1 = Punto(3, 4)
>>> p2 = Puntot(5, 7)
>>> p3 = p1 + p2
>>> print p3
(8, 11)
La expresión p1 + p2 es equivalente a p1.__add__(p2), aunque, obviamente, resulta más elegante.
Como ejercicio, suma un método __sub__(self, otro) que sobrecargue el operador de la resta, e inténtalo.
Existen varias formas de imponer el funcionamiento del operador de la multiplicación: definiendo un método llamado __mul__, o __rmul__, o ambos.
Si el operando izquierdo de * es un Punto, Python invoca __mul__, que asume que el otro operando también es un Punto. Calcula el producto escalar de dos puntos definidos de acuerdo con las reglas del álgebra lineal:
def __mul__(self, otro):
return self.x * otro.x + self.y * otro.y
Si el operando izquierdo de * es un tipo primitivo y el operando derecho es un Punto, Python invoca __rmul__, que efectúa una multiplicación escalar:
def __mul__(self, otro):
return Punto(otro * self.x, otro * self.y)
El resultado es un nuevo Punto cuyas coordenadas son múltiplo de las coordenadas originales. Si otro es un tipo que no puede multiplicarse por un número con coma decimal, __rmul__ dará un error.
Este ejemplo muestra ambos tipos de multiplicación:
>>> p1 = Punto(3, 4)
>>> p2 = Punto(5, 7)
>>> print p1 * p2
43
>>> print 2 * p2
(10, 14)
¿Qué pasaría si intentáramos analizar p2 * 2? Dado que el primer parámetro es un Punto, Python invoca __mul__ con 2 como el segundo argumento. Dentro de __mul__, el programa intenta acceder a la coordenada x de otro, pero no lo consigue porque una división no tiene atributos:
>>> print p2 * 2
AttributeError: 'int' object has no attribute 'x'
Por desgracia, el mensaje es poco claro. Este ejemplo muestra algunas de las dificultades de la programación orientada a objetos. A veces, ya es bastante difícil imaginarse qué código se está ejecutando.
Para ver un ejemplo más completo de la sobrecarga del operador, véase Apéndice B.
Comentarios
Muchos de los métodos que hemos escrito solo funcionan para un tipo específico. Cuando creas un objeto nuevo, escribes métodos que operan en ese tipo.
Pero existen ciertas operaciones que necesitarás aplicar a muchos tipos, como las operaciones aritméticas de los apartados anteriores. Si muchos tipos admiten el mismo conjunto de operaciones, puedes escribir funciones que sirvan en algunos de esos tipos.
Por ejemplo, la operación multisuma, que es común en álgebra lineal, toma tres parámetros; multiplica los dos primeros y luego suma el tercero. Podemos escribirlo en Phyton de la siguiente manera:
def multisuma (x, y, z):
return x * y + z
Este método funcionará con algunos valores de x e y que pueden multiplicarse y con cualquier valor de z que se le puede sumar al producto.
Podemos invocarlo con valores numéricos:
>>> multadd (3, 2, 1)
7
O con Puntos:
>>> p1 = Punto(3, 4)
>>> p2 = Punto(5, 7)
>>> print multadd (2, p1, p2)
(11, 15)
>>> print multadd (p1, p2, 1)
44
En el primer caso, el Punto se multiplica por un escalar y luego se le suma a otro Punto. En el segundo caso, el producto escalar da como resultado un valor numérico, de este modo, el tercer parámetro también tiene que ser un valor numérico.
Una función como ésta, que puede tomar parámetros de distintos tipos, se llama polimórfica.
Otro ejemplo sería el método adelanteAtras, que escribe la lista dos veces, hacia delante y hacia atrás:
def adelanteAtras(delante):
import copy
detrás = copy.copy(delante)
detrás.reverse()
print str(delante) + str(detrás)
Debido a que el método reverse es un modificador, podemos hacer una copia de la lista antes de invertirla. De esta manera, este método no modifica la lista, sino que la obtiene como un parámetro.
He aquí un ejemplo que aplica adelanteAtras a una lista:
>>> miLista = [1, 2, 3, 4]
>>> frontAndBack(miLista)
[1, 2, 3, 4][4, 3, 2, 1]
Obviamente, tenemos la intención de aplicar esta función a las listas, así que no es raro que funcione. Lo que sería sorprendente es que la pudiéramos aplicar a un Punto.
Para determinar si una función se puede aplicar a un nuevo tipo, aplicamos la regla fundamental de polimorfismo:
Si se pueden aplicar todas las operaciones dentro de la función al tipo, la función puede aplicarse al tipo.
Las operaciones en el método incluyen copy, reverse y print.
copy funciona con cualquier objeto y ya hemos escrito un método __str__ para Puntos, por lo tanto, lo que necesitamos es un método reverse en la clase Punto:
def reverse(self):
self.x , self.y = self.y, self.x
Luego podemos pasar Puntos a adelanteAtras:
>>> p = Punto(3, 4)
>>> adelanteAtras(p)
(3, 4)(4, 3)
El mejor tipo de polimorfismo es el involuntario, en donde se puede descubrir que una función, que ya se haya escrito, puede aplicarse a un tipo en el que nunca se había pensado.
Comentarios
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |