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)