Algoritmos de Programación con Python

14.3. Definiendo nuevos tipos

Sin bien Python nos provee con un gran número de tipos ya definidos, en muchas situaciones utilizar solamente los tipos provistos por el lenguaje resultará insuficiente. En estas situaciones queremos poder crear nuestros propios tipos, que almacenen la información relevante para el problema a resolver y contengan las funciones para operar con esa información.

Por ejemplo, si se quiere representar un punto en el plano, es posible hacerlo mediante una tupla de dos elementos, pero esta implementación es limitada, ya que si se quiere poder operar con distintos puntos (sumarlos, restarlos o calcular la distancia entre ellos) se deberán tener funciones sueltas para realizar las diversas operaciones.

Podemos hacer algo mejor definiendo un nuevo tipo Punto, que almacene la información relacionada con el punto, y contenga las operaciones nos interese realizar sobre él.

14.3.1. Nuestra primera clase: Punto

Queremos definir nuestra clase que represente un punto en el plano. Lo primero que debemos notar es que existen varias formas de representar un punto en el plano, por ejemplo, coordenadas polares o coordenadas cartesianas. Además, existen varias operaciones que se pueden realizar sobre un punto del plano, e implementarlas todas podría llevar mucho tiempo.

En esta primera implementación, optaremos por utilizar la representación de coordenadas cartesianas, e iremos implementando las operaciones a medida que las vayamos necesitando. En primer lugar, creamos una clase Punto que simplemente almacena las coordenadas.

class Punto(object):
    """ Representación de un punto en el plano, los atributos son x e y
        que representan los valores de las coordenadas cartesianas."""
    def __init__(self, x=0, y=0):
        "Constructor de Punto, x e y deben ser numéricos"
        self.x = x
        self.y = y

En la primera línea de código indicamos que vamos a crear una nueva clase, llamada Punto. La palabra object entre paréntesis indica que la clase que estamos creando es un objeto básico, no está basado en ningún objeto más complejo.

Nota Por convención, en los nombres de las clases definidas por el programador, se escribe cada palabra del nombre con la primera letra en mayúsculas. Ejemplos: Punto, ListaEnlazada, Hotel.

Además definimos uno de los métodos especiales, __init__, el constructor de la clase. Este método se llama cada vez que se crea una nueva instancia de la clase.

Este método, al igual que todos los métodos de cualquier clase, recibe como primer parámetro a la instancia sobre la que está trabajando. Por convención a ese primer parámetro se lo suele llamar self (que podríamos traducir como yo mismo), pero puede llamarse de cualquier forma.

Para definir atributos, basta con definir una variable dentro de la instancia, es una buena idea definir todos los atributos de nuestras instancias en el constructor, de modo que se creen con algún valor válido. En nuestro ejemplo self.x y self.y y se usarán como punto.x y punto.y.

Para utilizar esta clase que acabamos de definir, lo haremos de la siguiente forma:

>>> p = Punto(5,7)
>>> print p
<__main__.Punto object at 0x8e4e24c>
>>> print p.x
5
>>> print p.y
7

Al realizar la llamada Punto(5,7), se creó un nuevo punto, y se almacenó una referencia a ese punto en la variable p. 5 y 7 son los valores que se asignaron a x e y respectivamente.

Si bien nosotros no lo invocamos explícitamente, internamente Python realizó la llamada al método __init__, asignando así los valores de la forma que se indica en el constructor.

14.3.2. Agregando validaciones al constructor

Hemos creado una clase Punto que permite guardar valores x e y. Sin embargo, por más que en la documentación se indique que los valores deben ser numéricos, el código mostrado hasta ahora no impide que a x e y se les asigne un valor cualquiera, no numérico.

>>> q = Punto("A", True)
>>> print q.x
A
>>> print q.y
True

Si queremos impedir que esto suceda, debemos agregar validaciones al constructor, como las vistas en unidades anteriores.

Verificaremos que los valores pasados para x e y sean numéricos, utilizando la función es_numero, que incluiremos en un módulo llamado validaciones:

def es_numero(valor):
    """ Indica si un valor es numérico o no. """
    return isinstance(valor, (int, float, long, complex) )

Y en el caso de que alguno de los valores no sea numérico, lanzaremos una excepción del tipo TypeError. El nuevo constructor quedará así:

def __init__(self, x=0, y=0):
    """ Constructor de Punto, x e y deben ser numéricos,
        de no ser así, se levanta una excepción TypeError """
    if es_numero(x) and es_numero(y):
        self.x=x
        self.y=y
    else:
        raise TypeError("x e y deben ser valores numéricos")

Este constructor impide que se creen instancias con valores inválidos para x e y.

>>> p = Punto("A", True)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "<stdin>", line 11, in __init__
TypeError: x e y deben ser valores numéricos

Nota Python cuenta con un listado de excepciones que se pueden lanzar ante distintas situaciones, en este caso se utilizó TypeError que es la excepción que se lanza cuando una operación o función interna se aplica a un objeto de tipo inadecuado. El valor asociado es una cadena con detalles de la incoherencia de tipos.

Otra excepción que podríamos querer utilizar es ValueError, que se lanza cuando una operación o función interna recibe un argumento del tipo correcto, pero con un valor inapropiado y no es posible describir la situación con una excepción más precisa.

Si la situación excepcional que queremos indicar no está cubierta por ninguna de las excepciones del lenguaje, podremos crear nuestra propia excepción.

El listado completo de las excepciones provistas por el lenguaje se encuentra en docs.python.org/library/exceptions

14.3.3. Agregando operaciones

Hasta ahora hemos creado una clase Punto que permite construirla con un par de valores, que deben ser sí o sí numéricos, pero no podemos operar con esos valores. Para apreciar la potencia de los objetos, tenemos que definir operaciones adicionales que vayamos a querer realizar sobre esos puntos.

Queremos, por ejemplo, poder calcular la distancia entre dos puntos. Para ello definimos un nuevo método distancia que recibe el punto de la instancia actual y el punto para el cual se quiere calcular la distancia.

def distancia(self, otro):
    """ Devuelve la distancia entre ambos puntos. """
    dx = self.x - otro.x
    dy = self.y - otro.y
    return (dx*dx + dy*dy)**0.5

Una vez agregado este método a la clase, será posible obtener la distancia entre dos puntos, de la siguiente manera:

>>> p = Punto(5,7)
>>> q = Punto(2,3)
>>> print p.distancia(q)
5.0

Podemos ver, sin embargo, que la operación para calcular la distancia incluye la operación de restar dos puntos y la de obtener la norma de un vector. Sería deseable incluir también estas dos operaciones dentro de la clase Punto.

Agregaremos, entonces, el método para restar dos puntos:

def restar(self, otro):
    """ Devuelve un nuevo punto, con la resta entre dos puntos. """
    return Punto(self.x - otro.x, self.y - otro.y)

La resta entre dos puntos es un nuevo punto. Es por ello que este método devuelve un nuevo punto, en lugar de modificar el punto actual.

A continuación, definimos el método para calcular la norma del vector que se forma uniendo un punto con el origen.

def norma(self):
    """ Devuelve la norma del vector que va desde el origen
        hasta el punto. """
    return (self.x*self.x + self.y*self.y)**0.5

En base a estos dos métodos podemos ahora volver a escribir el método distancia para que aproveche el código ambos:

def distancia(self, otro):
    """ Devuelve la distancia entre ambos puntos. """
    r = self.restar(otro)
    return r.norma()

En definitiva, hemos definido tres operaciones en la clase Punto, que nos sirve para calcular restas, normas de vectores al origen, y distancias entre puntos.

>>> p = Punto(5,7)
>>> q = Punto(2,3)
>>> r = p.restar(q)
>>> print r.x, r.y
3 4
>>> print r.norma()
5.0
>>> print q.distancia(r)
1.41421356237

Advertencia Cuando definimos los métodos que va a tener una determinada clase es importante tener en cuenta que el listado de métodos debe ser lo más conciso posible.

Es decir, si una clase tiene algunos métodos básicos que pueden combinarse para obtener distintos resultados, no queremos implementar toda posible combinación de llamadas a los métodos básicos, sino sólo los básicos y aquellas combinaciones que sean muy frecuentes, o en las que tenerlas como un método aparte implique una ventaja significativa en cuanto al tiempo de ejecución de la operación.

Este concepto se llama ortogonalidad de los métodos, basado en la idea de que cada método debe realizar una operación independiente de los otros. Entre las motivaciones que puede haber para agregar métodos que no sean ortogonales, se encuentran la simplicidad de uso y la eficiencia.


Copyright (c) 2011-2014 Rosita Wachenchauzer, Margarita Manterola, Maximiliano Curia, Marcos Medrano, Nicolás Paez. La copia y redistribución de esta página se permite bajo los términos de la licencia Creative Commons Atribución - Compartir Obras Derivadas Igual 3.0 siempre que se conserve esta nota de copyright.