##########################################################
###                 Herencia en Python                 ###
##########################################################

# Definimos una clase padre
class Persona:
    pass

# Creamos una clase hija que hereda de la padre
class Participante(Persona):
    pass

class Aprendiz(Persona):
    pass

print("Participante hereda de: ", Participante.__bases__)
print("Persona le hereda a: ", Persona.__subclasses__())

# filosofía DRY. 
# El principio DRY (Don't Repeat Yourself) 
# no repetir código de manera innecesaria. 

class Persona:
    def __init__(self, nombre, edad):
        self.nombre = nombre
        self.edad = edad

    # Método genérico pero con implementación particular
    def hablar(self):
        # Método vacío
        pass

    # Método genérico pero con implementación particular
    def moverse(self):
        # Método vacío
        pass

    # Método genérico con la misma implementación
    def describeme(self):
        print("Soy una persona del tipo", type(self).__name__)

# Participante hereda de Persona
class Participante(Persona):
    pass

Participante1 = Participante('Marta', 18)
Participante1.describeme()

class Participante(Persona):
    def hablar(self):
        print("Hablo venezolano!")
    def moverse(self):
        print("Caminando con 2 piernas")

class Trabajadorxs(Persona):
    def hablar(self):
        print("Hablo caraqueño!")
    def moverse(self):
        print("Caminando con 2 manos")

class Aprendiz(Persona):
    def hablar(self):
        print("Hablo maracucho!")
    def moverse(self):
        print("Volando")

    # Nuevo método
    def picar(self):
        print("Picar!")

#    Heredados directamente de la clase padre: describeme()
#    Heredados de la clase padre pero modificados: hablar() y moverse()
#    Creados en la clase hija por lo tanto no existentes en la clase padre: picar()

aprendiz4 = Aprendiz('Rafael', 10)
participante4 = Participante('Julia', 23)
trabajadorxs4 = Trabajadorxs('Karla', 19)

aprendiz4.hablar()
participante4.hablar()


participante4.describeme()
trabajadorxs4.describeme()

aprendiz4.picar()

# Uso de super()
# La función super() nos permite acceder a los métodos de la clase padre desde una de sus hijas. 

class Persona:
    def __init__(self, sector, edad):
        self.sector = sector
        self.edad = edad        
    def hablar(self):
        pass

    def moverse(self):
        pass

    def describeme(self):
        print("Soy una persona del tipo", type(self).__name__)

# Tal vez queramos que nuestro clase tenga un parámetro extra en el constructor. Para realizar esto 
# tenemos dos alternativas:

# Podemos crear un nuevo __init__ y guardar todas las variables una a una.
# O podemos usar super() para llamar al __init__ de la clase padre que ya aceptaba el sector y edad, y 
# sólo asignar la variable nueva manualmente.

class Participante(Persona):
    def __init__(self, sector, edad, cedula):
        # Alternativa 1
        # self.sector = sector
        # self.edad = edad
        # self.cedula = cedula

        # Alternativa 2
        super().__init__(sector, edad)
        self.cedula = cedula

Participante5 = Participante('Pùblico', 7, 30222555)
Participante5.sector
Participante5.edad
Participante5.cedula

# Herencia múltiple

class Clase1:
    pass
class Clase2:
    pass
class Clase3(Clase1, Clase2):
    pass

# Es posible también que una clase herede de otra clase y a su vez otra clase herede de la anterior.

class Clase1:
    pass
class Clase2(Clase1):
    pass
class Clase3(Clase2):
    pass

# Llegados a este punto nos podemos plantear lo siguiente. Vale, como sabemos de otros posts las clases 
# hijas heredan los métodos de las clases padre, pero también pueden reimplementarlos de manera distinta.
# Entonces, si llamo a un método que todas las clases tienen en común ¿a cuál se llama?. Pues bien, existe
# una forma de saberlo.

# La forma de saber a que método se llama es consultar el MRO o Method Order Resolution. Esta función nos
# devuelve una tupla con el orden de búsqueda de los métodos. Como era de esperar se empieza en la propia
# clase y se va subiendo hasta la clase padre, de izquierda a derecha.

class Clase1:
    pass
class Clase2:
    pass
class Clase3(Clase1, Clase2):
    pass

print(Clase3.__mro__)

class Clase1:
    pass
class Clase2:
    pass
class Clase3:
    pass
class Clase4(Clase1, Clase3, Clase2):
    pass
print(Clase4.__mro__)

# Junto con la herencia, la cohesión, abstracción, polimorfismo, acoplamiento y encapsulamiento son otros
# de los conceptos claves para entender la programación orientada a objetos.

##########################################################
###                 Abstración en Python               ###
###  nombres como documentación interna y su sintaxis  ###
##########################################################

##########################################################
###                 Acoplamiento en Python             ###
##########################################################

class Clase1:
    x = True
    pass

class Clase2:
    def mi_metodo(self, valor):
        if Clase1.x:
            self.valor = valor

mi_clase = Clase2()
mi_clase.mi_metodo("Hola")
mi_clase.valor

Clase1.x = False

##########################################################
###              Encapsulamiento en Python             ###
##########################################################

# Python por defecto no oculta los atributos y métodos de una clase al exterior.

class Clase:
    atributo_clase = "Bienvenida"
    def __init__(self, atributo_instancia):
        self.atributo_instancia = atributo_instancia

mi_clase = Clase("al INCES")
print(mi_clase.atributo_clase)
print(mi_clase.atributo_instancia)

class Clase:
    atributo_clase1 = "Hola"   # Accesible desde el exterior    (Público)
    __atributo_clase2 = "Hola" # No accesible                   (Privado)

    # No accesible desde el exterior
    def __mi_metodo(self):
        print("Haz algo")
        self.__variable = 0

    # Accesible desde el exterior
    def metodo_normal(self):
        # El método si es accesible desde el interior
        self.__mi_metodo()

mi_clase = Clase()
#mi_clase.__atributo_clase2         # Error! El atributo no es accesible
#mi_clase.__mi_metodo()             # Error! El método no es accesible
print(mi_clase.atributo_clase1)     # Ok!
mi_clase.metodo_normal()            # Ok!
# print(dir(mi_clase))

### NO RECOMENDABLE ###
print(mi_clase._Clase__atributo_clase2)
mi_clase._Clase__mi_metodo()
### NO RECOMENDABLE ###

##########################################################
###              Polimorfismo en Python                ###
##########################################################

