Introducción a la Programación Orientada a Objetos (POO)

En este notebook daremos una introducción general a la programación orientada a objetos en Python. Todo es referente a Python3 (la gran parte compatible con las versiones más recientes de Python2).

Las principales ventajas de la POO son la encapsulación y la reutilización.

Clases en Python

La clase mínima en Python es:


In [1]:
class Atom():
    pass

La forma habitual de crear una clase (tipo) es crear un objeto (variable) de ella:


In [2]:
atom = Atom()

En este ejemplo, la clase no recibe ningún tipo de configuración (parámetros). Lo habitual es que los objetos tengan unos parámetros que los definen y son definidos al crear el objeto. Por ejemplo, si queremos crear un átomo de Carbono, que tiene 6 electrónes y 6 protones.


In [3]:
class Atom():
    def __init__(self, electrons, protons):
        self.electrons = electrons
        self.protons = protons

carbon = Atom(electrons = 6, protons = 6)

Para poder configurar la clase en la creación de objetos, necesitamos el método de inicialización de clase "init". Este método, al igual que el esto de métodos de una clase, recibe como primer parámetro "self". Esta variable la gestiona de forma automática Python, y es una referencia al objeto que se ha creado.

Para añadir funcionalidad a una clase la forma de hacerlo es mediante métodos (funciones) de clase. Estos métodos tienen como diferencia con las funciones habituales de Python que reciben como primer parámetro "self".

Vamos a añadir dos nuevos métodos para consultar el número de electrones y de protones de un átomo.


In [4]:
class Atom():
    def __init__(self, electrons, protons):
        self.electrons = electrons
        self.protons = protons

    def get_electrons(self):
        return self.electrons
    
    def get_protons(self):
        return self.protons

carbon = Atom(electrons = 6, protons = 6)
print("The carbon atom has %i electrons" % carbon.get_electrons())
print("The carbon atom has %i protons" % carbon.get_electrons())


The carbon atom has 6 electrons
The carbon atom has 6 protons

Herencia

Las clases se puede crear a partir de otras clases base. Por ejemplo, vamos a crear una clase específica para representar a los átomos de Carbon.


In [5]:
class Carbon(Atom):
    def __init__(self):
        self.electrons = 6
        self.protons = 6
    
carbon = Carbon()
print("The carbon atom has %i electrons" % carbon.get_electrons())
print("The carbon atom has %i protons" % carbon.get_electrons())


The carbon atom has 6 electrons
The carbon atom has 6 protons

La nueva clase Carbon es una especialización de la clase general (base) Atom. Ya sabe el número de electrones y protones, por lo que no hay que indicarlos. Y puede utilizar toda la funciolidad que ofrece la clase base.

La clase Carbon puede redefinir como se comparta la clase base allá donde lo necesite. En el ejemplo anterior, redefine la inicialización para poner el número de electrónes y protones, pero reutiliza el resto de métodos de Atom (get_electrons y get_protons).

La clase Carbon además puede añadir nuevos métodos para implementar funcionalidad específica del átomo de Carbon.

El átomo de Carbon tiene hasta 11 isotopos pero el más habitual es el Carbono12, que tiene 6 neutrones en el núcleo.

Podemos crear una nueva clase Carbon12 para modelar a este isotopo.


In [6]:
class Carbon12(Carbon):
    def __init__(self):
        self.electrons = 6
        self.protons = 6
        self.neutrons = 6

En general, todos los átomos tienen neutrones en eĺ núcleo, por lo que deberíamos de incluir como parámetro de Atom el número de neutrones, y un método para obtenerlos. Y en vez de crear una nueva clase por casa isótopo, esta información puede estar directamente en la clase Carbon, relejada en el número de neutrones. Con eso quedaría:


In [8]:
class Atom():
    def __init__(self, electrons, protons, neutrons):
        self.electrons = electrons
        self.protons = protons
        self.neutrons = neutrons
        
    def get_electrons(self):
        return self.electrons
    
    def get_protons(self):
        return self.protons
    
    def get_neutrons(self):
        return self.neutrons

class Carbon(Atom):
    def __init__(self, neutrons=6):
        self.electrons = 6
        self.protons = 6
        self.neutrons = neutrons

Ahora cuando creamos por defecto un átomo de carbón por defecto se crea con 6 neutrones, es decir, es el Carbon12. Pero si queremos otro isótopo de carbón, basta con indicarlo al construir el objeto carbón. Por ejemplo:


In [9]:
carbon13 = Carbon(neutrons=7)
print(carbon13.get_neutrons())


7

Métodos y Variables estáticas (de clase)

En realidad, analizando la clase Carbon, el número de electrones y de protones siempre es el mismo, independientemente del isótopo de carbón que sea, de la instanacia (objeto) de carbón que sea. Es decir, que esos datos no son específicos de los objetos, sino que están asociados a la clase y son los mismos en todos los objetos.

A estas variables asociadas a la clase se las llama variables estáticas. Y se declaran en el ámbito de clase, es decir:


In [14]:
class Atom():
    electrons = None
    protons = None

    def __init__(self, neutrons):
        self.neutrons = neutrons
    
    @classmethod
    def get_electrons(cls):
        print(cls)
        return cls.electrons
    
    @classmethod
    def get_protons(cls):
        return cls.protons
    
    def get_neutrons(self):
        return self.neutrons

class Carbon(Atom):
    electrons = 6
    protons = 6

    def __init__(self, neutrons=6):
        self.neutrons = neutrons
Junto a las variables estáticas, tenemos dos métodos estáticos que son los que en su implementación sólo necesitan acceder a variables estáticas, u otras variables no relacionadas con un objeto específico. Los métodos estáticos en vez de invocarse sobre la instancia del objeto, se invocan directamente sobre la clase.

In [16]:
carbon = Carbon()
print(Carbon.get_electrons())
print(carbon.get_electrons())


<class '__main__.Carbon'>
6
<class '__main__.Carbon'>
6

Python ayuda a que se puede llamar directamente a los métodos estáticos utilizando la instancia del objeto: de forma automática transforma la instancia del objeto en la clase a la que pertenece.

Métodos privados

Es posible declarar métodos privados dentro de una clase, de forma que no se pueda utilizar dicho método desde fuera de la propia clase. Por ejemplo:


In [22]:
class Atom():
    electrons = None
    protons = None

    def __init__(self, neutrons=None):
        self.neutrons = neutrons
        self.__atom_private()
    
    @classmethod
    def get_electrons(cls):
        print(cls)
        return cls.electrons
    
    @classmethod
    def get_protons(cls):
        return cls.protons
    
    def get_neutrons(self):
        return self.neutrons
    
    def __atom_private(self):
        print("Atom private method")

class Carbon(Atom):
    electrons = 6
    protons = 6

    def __init__(self, neutrons=6):
        self.neutrons = neutrons
        self.__atom_private()

atom = Atom()
carbon = Carbon()


Atom private method
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-22-c81ec06659c4> in <module>()
     31 
     32 atom = Atom()
---> 33 carbon = Carbon()

<ipython-input-22-c81ec06659c4> in __init__(self, neutrons)
     28     def __init__(self, neutrons=6):
     29         self.neutrons = neutrons
---> 30         self.__atom_private()
     31 
     32 atom = Atom()

AttributeError: 'Carbon' object has no attribute '_Carbon__atom_private'

UML

Diagram

Práctica de Clases en Python

Hasta ahora hemos tratado de átomos. El objetivo de la práctica es diseñar el concepto de molécula y modelar como podríamos definir la molécula de agua (H2O) y la de oxígeno (O2).