Programación orientada a objetos con Python =========================================== Python permite varios **paradigmas de programación**, incluyendo la programación orientada a objetos (POO). La POO es una manera de estructurar el código que le hace especiamente efectivo organizando y reutilizando código, aunque su naturaleza abstracta hace que no sea muy intuitivo cuando se empieza. La programación `orientada a objetos en Python `__ es opcional y de hecho hasta ahora no la hemos usado directamente, aunque indirectamente lo hemos hecho desde el principio. Aunque su mayor ventaja aparece con los programas largos y más complejos, es muy útil entender cómo funciona la POO, ya que es así como Python funciona internamente. En general, los objetos se pueden considerar como tipos de datos con características propias que también pueden tener funcionalidades propias. Por ejemplo, un variable tipo *string* es un objeto que tiene algunas propidades, como su longitud (`len(str)`) y al que se pueden aplicar funciones específicas, llamadas métodos, como `str.title()` o `str.replace()`. De forma similar podemos crear nuestros propios objetos con características (llamadas artributos) y métodos propios. Los objetos se definen usando clases (`class()`) y las variables que de definen en ella son propiedades comunes de ese objeto. Por ejemplo, consideremos un objeto tipo estrella llamado `Star`, la clase más sencilla para crearla sería así: :: # Creamos stars.py class Star: """Clase para estrellas""" tipo = "Estrella" Sin embargo, este objeto no es muy útil porque todos los objetos creados serán iguales. Podemos empezar incluyendo artributos propios de cada objeto creado, como su nombre, por ejemplo: :: class Star: """Clase para estrellas""" def __init__(self, name): self.name = name # Método especial que se llama cuando se hace print def __str__(self): return "Estrella {}".format(self.name) La clase tiene una función principal especial ``__init__()`` que construye el elemento de la clase ``Star`` (llamado *objeto*) y que se ejecuta cuando crea un nuevo objeto o instancia de esa clase; hemos puesto ``name`` como parámetro único obligatorio, pero no tiene porqué tener ninguno. La variable especial ``self`` con la que empieza cada función (llamadas métodos en la POO), se refiere al objeto en concreto que estamos creando, esto se verá más claro con un ejemplo. Ahora ya podemos crear objetos tipo ``Star``: :: # Librería star.py que incluye la clase Star import stars # Instancia (objeto) nueva de Star, con un parámetro (el nombre), obligatorio estrella1 = stars.Star("Altair") # Lo que devuelve al imprimir el objeto, según el método __str__ print(estrella1) # Estrella Altair print(estrella1.name) # Altair Al crear el objeto con nombre ``estrella1``, que en la definición de la clase llamamos ``self``, tenemos un tipo de dato nuevo con la propiedad ``name``. Ahora podemos añadir algunos métodos que se pueden aplicar al objeto ``Star``: :: class Star: """Clase para estrellas Ejemplo de clases con Python Fichero: stars.py """ # Numero total de estrellas num_stars = 0 def __init__(self, name): self.name = name Star.num_stars += 1 def set_mag(self, mag): self.mag = mag def set_par(self, par): """Asigna paralaje en segundos de arco""" self.par = par def get_mag(self): print("La magnitud de {} de {}".format(self.name, self.mag)) def get_dist(self): """Calcula la distancia en parsec a partir de la paralaje""" print("La distacia de {} es {:.2f} pc".format(self.name, 1/self.par)) def get_stars_number(self): print("Numero total de estrellas: {}".format(Star.num_stars)) Ahora podemos hacer más cosas un objeto ``Star``: :: import stars # Creo una instancia de estrella altair = stars.Star('Altair') altair.name # Devuelve 'Altair' altair.set_par(0.195) altair.get_stars_number() # Devuelve: Numero total de estrellas: 1 # Uso un método general de la clase star.pc2ly(5.13) # Devuelve: 16.73406 altair.get_dist() # Devuelve: La distancia de Altair es 5.13 pc # Creo otra instancia de estrella otra = stars.Star('Vega') otra.get_stars_number() # Devuelve: Numero total de estrellas: 2 altair.get_stars_number() # Devuelve: Numero total de estrellas: 2 ¿No resulta familiar todo esto? es similar a los métodos y propiedades de elementos de Python como *strings* o listas, que también son objetos definidos en clases con sus métodos. Los objetos tienen una interesante propiedad llamada **herencia** que permite reutilizar propiedades de otros objeto. Supongamos que nos interesa un tipo de estrella en particular llamada *enana blanca*, que son estrellas ``Star`` con algunas propiedades especiales, por lo que necesitaremos todas las propiedades del objeto ``Star`` y alguna nueva que añadiremos: :: class WBStar(Star): """Clase para Enanas Blancas (WD)""" def __init__(self, name, type): """Tipo de WD: dA, dB, dC, dO, dZ, dQ""" self.name = name self.type = type Star.num_stars += 1 def get_type(self): return self.type def __str__(self): return "Enana Blanca {} de tipo {}".format(self.name, self.type) Ahora, como parámetro de ``class`` hemos puesto ``Star`` para que **herede** las propiedades de esa clase. Así, al crear un objeto ``WDStar`` estamos creando un objeto distinto, con todas las propiedades y métodos de ``Star`` y una propiedad nueva llamada ``type``. Además sobrescribimos el resultado al imprimir con ``print`` definiendo el método especial ``__str__``. Como vemos, los métodos, que son las funciones asociadas a los objetos, sólo se aplican a ellos. Si en nuestro fichero la clase, que hemos llamado ``stars.py`` y que contiene por ahora las clases ``Star`` y ``WDStar`` añadimos una función normal, ésta se puede usar de forma habitual: :: class Star(Star): ... class WBStar(Star): ... def pc2ly(dist): """Convierte parsec a anhos luz""" return dist*3.262 Y como hasta ahora: :: import stars # Convierte parsecs en años luz distancia_ly = stars.Star.pc2ly(10.0)