17 de mayo de 2016

31. La clase "punto"

Una clase "propia", por ejemplo, la clase punto.

Los tipos hemos visto que son clases de Python. A veces, al programar, puede resultar insuficiente estas clases o puede ser muy deseable enfocar nuestra programación en el desarrollo de nuevos objetos y debemos crear nuestras clases.
Por ejemplo, imaginemos que queremos crear un programa que sea capaz de manejar vectores en un plano. Un vector fijo está definido por dos puntos, y los puntos son representados en un plano por las coordenadas respectos de sus ejes X e Y (si son coordenadas cartesianas), o mediante su distancia al centro y el ángulo respecto de uno de sus ejes (si sus coordenadas son polares).
Así que podemos tener muchas entidades llamadas puntos. Bien, representémoslas por una clase en Python
# *-* coding:utf-8 *-*
# clase puntos
class punto:
    
    def __init__(self):
        self.x = 0.0
        self.y = 0.0
    
    def imprimir(self):
        print("[%.2f, %.2f]") % (self.x, self.y)

# Programa principal
# Punto A
A = punto()

# Punto B
B = punto()

# Imprime los puntos
A.imprimir()
B.imprimir()
Como podemos notar, tenemos definidos dos métodos: uno que se ejecuta al crear la instancia de cada punto - constructor - y que establece sus coordenadas en 0,0 y otro que imprime sus coordenadas. Añadamos más funcionalidad a esta clase.
= = =
1. Mejor de esta manera...
De esta forma puedo pasar a la clase, a su constructor (o no hacerlo) los valores de inicio de los objetos.
# clase puntos
class punto(object):
    """ Clase que permite trabajar con puntos en el plano, 
    en coordenadas cartesianas, x e y"""
    
    def __init__(self, x0=0.0, y0=0.0):
        """ pre: los puntos deben definirse con sus coordenadas x
        e y - tipo float - y pueden establecerse en la llamada inicial.
        La notación superior x0=0.0 e y0=0.0 indica los valores iniciales
        si no se define en la llamada """
        self.x = x0
        self.y = y0
    
    def imprimir(self):
        print("[%.2f, %.2f]") % (self.x, self.y)

# Programa principal
# Punto A, # Defino valores al principio
A = punto(6,7)

# Punto B. Sin definir valores al principio.
B = punto()

# Imprime los puntos
A.imprimir()
B.imprimir()
= = =
2.- Agregando validaciones
# *-* coding:utf-8 *-*

# Validaciones
def es_numero(num):
    # indica si es o no un valor numérico
    return isinstance (num, (int, float, complex, long))

# clase puntos
class punto(object):
    """ Clase que permite trabajar con puntos en el plano, 
    en coordenadas cartesianas, x e y"""
    
    def __init__(self, x0=0.0, y0=0.0):
        """ pre: los puntos deben definirse con sus coordenadas x
        e y - tipo float - y pueden establecerse en la llamada inicial.
        La notación superior x0=0.0 e y0=0.0 indica los valores iniciales
        si no se define en la llamada """
        if es_numero(x0) and es_numero(y0):
            self.x = x0
            self.y = y0
        else:
            raise TypeError("x e y deben ser números")
    
    def imprimir(self):
        print("[%.2f, %.2f]") % (self.x, self.y)

# Programa principal
# Punto A, # Defino valores al principio
A = punto(6,7)

# Punto B. Sin definir valores al principio.
B = punto("A", True)

# Imprime los puntos
A.imprimir()
B.imprimir()
Conviene agregar validaciones para evitar obtener en el programa datos de tipo distinto a los necesarios
= = =
3.- Añadiendo funcionalidad
Podemos ir agregando varios métodos. Entre ellos el cálculo de la distancia a otro punto:
    def distancia(self,otro):
        """Devuelve la distancia entre dos puntos"""
        dx = self.x-otro.x
        dy = self.y-otro.y
        return (dx*dx+dy*dy)**(0.5)
Sin embargo, esta función implica calcular: a) la resta de dos vectores y b) la norma de un vector. Así que es mejor agregar más métodos y así poder obtener más funcionalidad de la clase.
    def resta(self,otro):
        """Devuelve un punto que es la resta de otros dos"""
        dx = self.x-otro.x
        dy = self.y-otro.y
        return punto(dx,dy)
        
    def norma(self):
        """Devuelve la norma de un vector"""
        return (self.x ** 2 + self.y ** 2) ** (0.5)
        
    def distancia(self, otro):
        """Devuelve la distancia entre dos puntos """
        vectorResta = self.resta(otro)
        return vectorResta.norma()
= = =
4.- El método especial __str__
Ya hemos visto el constructor, el método especial que se llama cuando se inicializa la clase (__init__). Ahora estudiaremos el método __str__, que nos permitirá formatear la presentación del objeto cuando sea invocado con una llamada de cadena como str o print.
Modificamos el método imprimir y añadimos el método __str__
    def imprimir(self):
        print self
                
    def __str__(self):
        """ Muestra el punto como un par ordenado. """
        return "[" + str(self.x) + ", " + str(self.y) + "])"
= = =
5.- Más métodos especiales, __add__ y __sub__
Hemos definido la resta. Pero cada vez que la invoquemos no podemos llamarla más que de la forma que he definido
# resta de los dos puntos
C = A.resta(B)
# ¡¡No puedo hacer C = A - B!!
print C
¿O no? Pues puedo hacerlo con un método especial, __sub__ que me permitirá que la clase sepa qué hacer cuando utilice el operador "-".
    def __sub__(self, otro):
        """Devuelve un punto que es la resta de otros dos"""
        dx = self.x-otro.x
        dy = self.y-otro.y
        return punto(dx,dy)
¡¡ Y ahora puedes ya hacer C = A - B y print C !! (NOTA: a la vista de la función __sub__ puede deducirse que no es necesaria la función resta). Y asimismo puede construir la función __add__ que es la suma de las coordenadas y se puede redefinir cada operación matemática para que actúe sobre los objetos de una clase.
A la redefinición de operadores matemáticos dentro de una clase se le denomina sobrecarga de operadores y no todos los lenguajes de programación soportan esta característica.

Otra vuelta de tuerca a las clases y a los objetos.

Vamos a sacarle más partido de las clases. En el código puntos2.py encontramos lo siguiente (que no estaba en el código puntos.py):
1.- Multiplicar un punto por un factor.
    def __mul__(self, factor):
        """ Multiplicar un número o factor por un vector o punto """
        dx = factor * self.x
        dy = factor * self.y
        return punto (dx,dy)
En este método, se define la multiplicación de un punto por un escalar. ¡Cuidado! ¡¡No es conmutativo!! El primer operando es el punto y el segundo el factor, por lo que se debe llamar como C = A * 5, nunca como C = 5 * A. Se ha perdido la propiedad conmutativa ya que el primer parámetro que debe pasar a una función es un objeto de la clase al que pertenece el método.
= = =
2.- Suma de dos puntos.
    def __add__(self,otro):
        """Suma las coordenadas de dos puntos"""
        return self - otro * (-1)
Definida la multiplicación por un factor, la suma se puede expresar como la resta con signo cambiado de puntos
= = =
3.- Comparación
    def __cmp__(self, otro):
        """ Comparo dos puntos por su norma """
        """ Si es el primero de norma menor que el segundo devuelve -1
        Si es mayor, devuelve 1
        Si es igual, devuelve 0"""
        diferencia = self.norma() - otro.norma()
        if diferencia < 0:
            return -1
        elif diferencia > 0:
            return 1
        else:
            return 0
La función de comparación permite usar criterios como igualdad y mayor - menor que. También ordenar listas. En este ejemplo, un poco traído por los pelos, ya que dos vectores definidos por sus coordenadas no son "mayores" o "menores" que otro por su norma (sólo sus módulos), permite comparar los puntos por sus normas al origen.
Se calcula la diferencia, entre ellos y, si la diferencia es mayor que 0 retorna 1, que significa que el primero es mayor que el segundo; -1, y lo contrario, que es menor y 0 si son iguales.
print "A mayor que B: ", A > B
print "A menor que B: ", A < B
print "A igual a B: ", A == B
= = =
4.- Ordenar listas de objetos de una clase
Por último, y siguiendo lo que haya programado en el bloque __cmp__ , se ordenan en una lista los elementos con el criterio sort.
# ordenar lista según su norma
# El método sort utiliza internamente el método de comparación
C = punto(0,4)
lista = [A , B, C]
lista.sort()

for i in lista:
    print i
= = =

Se puede definir un método en una clase o una función que sirva como criterio de comparación en una lista...
lista.sort(cmp = punto.funcion_ordenar)

Y algo más

Comparar puntos por la norma es algo traído por los pelos. Lo que sí es legítimo es comprobar si dos puntos son iguales. Eso se puede determinar con los métodos __eq__ y __ne__.
    def __eq__(self,otro):
        """ Determina si dos puntos son iguales """
        return (self.x == otro.x and self.y == otro.y)
        
    def __ne__(self,otro):
        """ Si dos puntos son distintos """
        return not self == otro

. . . 

C = punto(2,4)

print "¿Son iguales (2,4) y A - B?" , C == A - B, A - B
print "¿Son iguales (2,4) y A + B?" , C == A + B, A + B


No hay comentarios:

Publicar un comentario