Python aun mas OOP

TL;DR Vamos a continuar con nuestra programación orientada a objetos aplicada en python, hoy aprendemos un poco mas sobre la herencia de objetos.

Hola! bienvenidos a este segundo post sobre el OOP de python, como recordaremos de nuestro primer post en python podemos utilizar la porgramación orientada objetos para poder crear código reutilizable y muy facil de leer.

El concepto de herencia en la programación orientada a objetos se refiere a la creación de una nueva clase a partir de una clase padre, esta compartirá los atributos y funciones de la clase padre y podrá crear nuevos comportamientos dentro de ella, ya sea agregando nuevas funciones y atributos o “reescribiendo” las funciones del padre.

La herencia mas sencilla y la primera que siempre usaremos es la herencia a la clase “objeto” de Python, recordemos que todo en python es un objeto.

class Building(object):
    pass

De esta manera, podemos crear una clase que hereda de la clase “maestra” de python la cual es object.

Para entender un poco sobre que “metodos/funciones” y propiedades son heredadas de una clase padre a una clase hijo, hagamos el siguiente ejemplo.

Un edificio “estandar” tiene unas puertas las cuales para abrir hay que hacer la accion de empujar, digamos que esto modelado al OOP se veria parecido a esto.

class Building(object):
    n_floors = 7
    n_elevators = 2
    def openDoor():
        print "You need to push to open"

Pero los nuevos edificios ya no debes empujar, disponemos de puertas electricas que debo solo estar frente de ellas para que se abran 😀 esto es super cool, pero digamos que aparte de las puertas todo lo demas del edificio sigue siendo igual, tienen numeros de pisos, elevadores, etc. entonces para poder mantener las mismas “caracteristicas” o propiedades, decidimos heredar del edificio convencional y crear nuestro nuevo tipo (clase) de edificio de la siguiente manera

class NewBuilding(Building):
    def openDoor():
        print "just walk to me, i will open automatically :D"

De esta manera, las nuevas instancias de NewBuilding mantendrán los atributos de “n_floor” para controlar el numero de pisos, o “n_elevators” para saber cuantos elevadores disponemos, pero la accion de abrir la puerta sera un poco diferente a su padre, a esto le llamamos “Sobreescritura de metodos”.

Otra caracteristica importante en la OOP que discutimos en nuestro post anterior es la “inicialización” de nuestras instancias a través del metodo __init__, esto para poder pasar como parametros los atributos iniciales de nuestra clase, una practica muy común en python es “sobreescribir” el init para cambiar el comportaminto de inicialización de una clase hija, ¡pero! conservando la inicialización del padre, para esto utilizamos la funcion con super poderes, si la funcion “super”, ya veremos que realiza la funcion super.

Supongamos que necesitamos inicializar un parametro extra dentro de nuestra nueva clase NewBuilding, ademas de los pisos (n_floors) y numero de elevadores (n_elevators) necesitamos un nuevo parametro para saber cuantas oficinas por piso hay en nuestro edificio. para esto escribiremos la clase padre e ilustraremos como sería incluir este nuevo parametro en la clase hijo.

class Building(object):
    def __init__(self, n_floors=7, n_elevators=2):
        self.n_floors = n_floors
        self.n_elevators = n_elevators
    
    def openDoor(self):
        print "You need to push to open"

Cambiamos un poco la inicialización de nuestro padre Building, ya no tenemos los atributos con su valor inicial en el cuerpo de la clase, pero mantenemos la escencia al enviar un valor por default en su funcion de inicialización, ahora veamos como debemos cambiar al hijo de manera que inicialice todo lo que necesita el padre, y agregar como extra lo que necesita nuestra nueva clase NewBuilding (hijo)

class NewBuilding(Building):
    def __init__(self, n_floors=7, n_elevators=2, n_offices=4):
        super(NewBuilding, self).__init__(n_floors, n_elevators)
        self.n_offices = n_offices
    
    def openDoor(self):
        print "just walk to me, i will open automatically :D"

Ok, que ha pasado aqui, ¿es un poco confuso no?, vamos, intentare explicarlo.

Hemos definido una funcion init, de manera que podamos escribir como se instanciará nuestra clase hija NewBuilding, hasta aquí, no deben haber sorpresas.

La siguiente linea es la que intriga, si, super es una funcion built-in en python la cual se utiliza para poder hacer llamados a metodos que se encuentran en la clase padre de nuestra clase actual, ¿sencillo?… si, ¿que neceista esta funcion? esta funcion necesita una clase como primer parametro para poder buscar cual es la clase padre de la clase actual, como segundo parametro necesita una instancia de esta clase, ¡Entiendo! por eso se pasa el self, ya todo se va aclarando, por ultimo al super construir una “instancia del padre” (podriamos decir asi en terminos para poder entender) podemos hacer llamado del metodo init del padre, pasando los parametros necesarios para esta funcion, por ende, al ejecutar la funcion init del padre, self es pasado como parametro y ejecuta todo lo que esta en la inicialización del padre. ¡Facil!, se ejecuto la inicializacion del padre ademas de la del hijo, la cual aun se ejecuta ¿no?.

Por ultimo en la funcion init del hijo se inicializa el atributo n_offices con el nuevo parametro de inicialización “n_offices” o se coloca el valor por defecto de 4 que se pasa por parametro en la firma de la funcion.

Hasta ahora es excelente como en python puedo reutilizar código heredando de una clase padre, pero ¿es posible tener mas de un padre para una clase?, en python ¡Si! pero hay que ser extremadamente cuidadoso, por eso vamos a aprender como funciona la multi-herencia de python ahora.

En python existe la multiple herencia, para crear una clase que herede de 2 o mas padres hay que especificar esto en la cabecera de la clase de la siguiente manera

class NewBuilding(ShoppingCenter, Building):
    def __init__(self, n_floors=7, n_elevators=2, n_offices=4):
        super(NewBuilding, self).__init__(n_floors, n_elevators)
        self.n_offices = n_offices
    
    def openDoor(self):
        print "just walk to me, i will open automatically :D"

En el anterior ejemplo, NewBuilding es hijo de ShoppingCenter y Building. Este tendrá tanto los metodos y atributos de las 2 clases, pero es importante aclarar que el orden de las clases a heredar es de suma importancia para la sobreescritura de metodos.

En la inea super(NewBuilding, self).__init__(n_floors, n_elevators) se llama en primera instancia el __init__ de la clase Building, y si la clase Building llama a su padre (con super) este invocará al init de la clase ShoppingCenter, esta cascada va de derecha a izquierda.

¿Un poco confuso no?, para poder entender esto realizaré un Post sobre el tema mas utilizado cuando se realiza multi-herencia, y esto son los Mixins.

Espero que hayan entendido la herencia en python y como podemos usarla para no repetir mucho código en cada clase que hacemos, la multi-herencia la explicare de mejor manera en el proximo Post! hasta la proxima 😀