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.