<jupyter_start><jupyter_text>El Objeto Un objeto es una unidad que engloba en sí mismo características y comportamiento necesarias para procesar información. Cadaobjeto contiene datos y funciones. Y un programa se construye como un conjunto de objetos, o como un único objetoEjemplo– Carro BMW    Características    – 4 Ruedas Micheline    – Motor BMW    – Caja de cambios de 7 Velocidades    – Color Azul    – 2 Espejos  Clases y ObjetosPython está completamente orientado a objetos: puede definir sus propias clases, heredar de las que usted defina o de las incorporadas en el lenguaje, e instanciar las clases que haya definido.En Python las clases se definen mediante la palabra reservada class seguida del nombre de la clase, dos puntos (:) y a continuación, indentado, el cuerpo de la clase. Ejemplo:    class Ejemplo:        pass        En este ejemplo ejemplo el nombre de la clase es Ejemplo Ejemplo y no hereda de otra clase. Por convención las clases empiezan en Mayúscula.Esta clase no define atributos pero no puede estar vacía para eso usamos la función pass, equivalente en otros lenguajes a usar {}.   El método init Las clases de Python no tienen constructores o destructores explícitos. Las clases de Python tienen algosimilar a un constructor: el método \__init__.<jupyter_code>import math

class Complejo:
    def __init__(self, real, imaginario):
        self.real = real
        self.img = imaginario
        
    def abs(self):
        print(math.sqrt((self.real ** 2)+(self.img ** 2)))
        
def main():
    complejo = Complejo(1, 2)
    complejo.abs()
    
    complejo2 = Complejo(2, 4)
    complejo2.abs()
    
main()<jupyter_output>2.23606797749979
4.47213595499958<jupyter_text>\__init__ se llama inmediatamente tras crear una instancia de la clase. Sería tentador pero incorrecto denominar a esto el constructor de la clase. Es tentador porque parece igual a un constructor (por convención, \__init__ es el primer método definido definido para la clase), clase), actúa como uno (es el primer pedazo de código que se ejecuta en una instancia de la clase recién creada), e incluso suena como uno. Incorrecto, porque el objeto ya ha sido construido para cuando se llama a \__init__, y ya tiene una referencia válida a la nueva instancia de la clase. Pero \__init__ es lo más parecido a un constructor que va a encontrar en Python, y cumple el mismo papel. El primer atributo o variable de cada método de clase, incluido \__init__, es siempre una referencia a la instancia actual de la clase.  Por convención, este argumento siempre se denomina self. En el método \__init__, self se refiere refiere al objeto recién creado; en otros métodos de la clase, se refiere a la instancia cuyo método ha sido llamado. Los métodos \__init__ pueden tomar cualquier cantidad de argumentos, e igual que las funciones, éstos pueden definirse con valores por defecto, haciéndoles opcionales para quien invoca. Por convención, el primer argumento de cualquier clase de Python (la referencia a la instancia) se denomina self.Cumple el papel de la palabra reservada this en C++ o Java, pero self no es una palabra palabra reservada reservada en Python, sino una mera convención. Aunque necesita especificar self de forma explícita cuando define el método, no se especifica al invocar el método; Python lo añadirá de forma automática.   Instanciación de las ClasesCrear un objeto o instanciar una clase en Python es muy sencillo. Para instanciar una clase, simplemente se invoca a la clase como si fuera una función, función, pasando pasando los argumentos argumentos que defina el método \__init__. El valor de retorno será el objeto recién creado.<jupyter_code>import math

class Complejo:
    def __init__(self, real, imaginario):
        self.real = real
        self.img = imaginario
        
    def abs(self):
        print(math.sqrt((self.real * self.real)+(self.img * self.img)))
        
def main():
    numero = Complejo(3, 4) # Se crea el objeto y se inicializa haciendo el llamado al método __init__ pasando 
                            # los valores real = 3 y imaginario = 4
    numero.abs() # convocamos al métodos abs de la clase complejo
    
main()<jupyter_output>5.0<jupyter_text>Atributos de datosPython admite atributos de datos (llamados variables de instancia en Java, y variables miembro en C++). Para hacer referencia a este atributo desde código que esté fuera de la clase, debe calificarlo con el nombre de la instancia, __instancia.data__, de la misma manera que calificaría calificaría una función función con el nombre de su módulo.  Para hacer referencia a atributos de datos desde dentro de la clase, use self como calificador. Por convención, todos los atributos de datos se inicializan en el método \__init__. Sin embargo, esto no es un requisito, ya que los atributos, al igual que las variables locales, comienzan a existir cuando se les asigna su primer valor.<jupyter_code>import math

class Complejo:
    def __init__(self, real, imaginario):
        self.real = real
        self.img = imaginario
        
    def abs(self):
        print(math.sqrt((self.real * self.real)+(self.img * self.img)))
        
def main():
    numero = Complejo(3, 4) # Se crea el objeto y se inicializa haciendo el llamado al método __init__ pasando 
                            # los valores real = 3 y imaginario = 4
    print(numero.real)      # variable de instancia real
    print(numero.img)       # variable de instancia imaginaria
    
    numero.abs()
    
main()<jupyter_output>3
4
5.0<jupyter_text>Sobre carga de métodosC++ y Java admiten la sobrecarga de funciones por lista de argumentos, es decir una clase puede tener varios métodos con el mismo nombre, pero con argumentos en distinta cantidad, o de distinto tipo. Python no admite sobrecarga de funciones. Los métodos se definen definen sólo por su nombre y hay un único método por clase con un nombre dado. De manera que si una clase sucesora tiene un método \__init__, siempre sustituye al método \__init__ de su clase padre, incluso si éste lo define con una lista de argumentos diferentes. Y se aplica lo mismo a cualquier otro método  Sobre carga de operadoresLa sobrecarga de operadores permite redefinir ciertos operadores, como "+" y "-", para usarlos con las clases que hemos definido. Se llama sobrecarga de operadores porque estamos reutilizando el mismo operador con un número de usos diferentes, y el compilador decide cómo usar ese operador dependiendo sobre qué opera.    • __add__( self, other)           -> Oper. Suma    • __sub__( self, other)           -> Oper. Resta    • __mul__( self, other)           -> Oper. Multiplicacion    • __rmul__( self, other)          -> Oper. Multi. Por Escalar    • __floordiv__( self, other)      -> Oper. division . division Redondeo Redondeo    • __mod__( self, other)           -> Oper. modulo    • __divmod__( self, other)        -> Oper. division    • __pow__( self, other[, modulo]) -> Oper. Potencia    • __and__( self, other)           -> Oper. and    • __xor__( self, other)           -> Oper. xor    • __or__( self, other)            -> Oper. or<jupyter_code>import math

class Complejo:
    def __init__(self, real, imaginario):
        self.real = real
        self.img = imaginario
        
    def abs(self):
        print(math.sqrt((self.real * self.real)+(self.img * self.img)))
        
    def __add__(self, otro):
        return Complejo(self.real + otro.real, self.img + otro.img)
    
    def __sub__(self, otro):
        return Complejo(self.real - otro.real, self.img - otro.img)
    
    def __str__(self):
        return f'{self.real} + {self.img}i'
        
def main():
    complejo1 = Complejo(8, 4)
    complejo2 = Complejo(3, 4)
    complejo3 = complejo1 + complejo2
    complejo4 = complejo1 - complejo2
    
    print(f'complejo1: {complejo1}')
    print(f'complejo3: {complejo3}')
    print(f'complejo3: {complejo4}')
    
main()<jupyter_output>complejo1: 8 + 4i
complejo3: 11 + 8i
complejo3: 5 + 0i<jupyter_text>Otro ejemplo<jupyter_code>class SuperficieCuadrada():
    '''Realiza operaciones con superficies de cuadrados.'''
    lado = 1
    
    def superficie(self):
        '''Calcula la superficie de un cuadrado.'''
        return self.lado ** 2
    
    def __add__(self, elemento):
        '''Realiza operaciones de suma.'''
        if type(elemento) is SuperficieCuadrada:
            return self.superficie() + elemento.superficie()
        elif type(elemento) in (int, float):
            return self.superficie() + elemento
        else:
            raise NotImplementedError
            
superficie1 = SuperficieCuadrada()
superficie1.lado = 2
superficie2 = SuperficieCuadrada()
superficie2.lado = 4

total = superficie1 + 23
print(total)
cuadros = (SuperficieCuadrada(), SuperficieCuadrada())
cuadros[0].lado = 25
cuadros[1].lado = 12

cuadros[0] + cuadros[1]
print(cuadros[0].superficie())
cuadros[0] + 12
cuadros[1] + 2j
12 + cuadros[0]<jupyter_output><empty_output><jupyter_text>Métodos de operadores reciprocosEn el ejemplo previo, el orden del operador tiene relevancia, ya que los objetos de tipo int y de tipo float, los cuales se invocan primero por estar a la izquierda del operador, no cuentan con una implementación de __add__ para la clase SuperficieCuadrada.Lo mismo ocurre con la función sum(), la cual comienza a realizar la suma desde 0, el cual es de tipo int.Cuando un objeto de tipo numérico no encuentra una implementación adecuada, busca un método recíproco en el objeto a la derecha del operador. En este caso busca al método __radd__().Los métodos de operador recíprocos de son:__radd__()__rdiv__()__rmod__()__rmul__()__rsub__()<jupyter_code>class SuperficieCuadrada():
    '''Realiza operaciones con superficies de cuadrados.'''
    lado = 1
    
    def superficie(self):
        '''Calcula la superfice de un cuadrado.'''
        return self.lado ** 2
    
    def __add__(self, elemento):
        '''Realiza operaciones de suma.'''
        if type(elemento) is SuperficieCuadrada:
            return self.superficie() + elemento.superficie()
        elif type(elemento) in (int, float):
            return self.superficie() + elemento
        else:
            raise NotImplementedError
            
    def __radd__(self, elemento):
        '''Realiza operaciones de suma cuando el otro objeto no la tiene implementada.'''
        return self.__add__(elemento)
cuadrados = [SuperficieCuadrada(), SuperficieCuadrada(), SuperficieCuadrada()]
cuadrados[0].lado = 13.5
cuadrados[1].lado = 50.33
cuadrados[2].lado = 23.1
12.5 + cuadrados[1]
sum(cuadrados)<jupyter_output><empty_output><jupyter_text>Métodos de clase En ocasiones es necesario contar con métodos que interactúen con elementos de la clase de la cual el objeto es instanciado. Python permite definir métodos de clase para esto.Los métodos de clase son aquellos que están ligados directamente con los atributos definidos en la clase que los contiene. Para definir un método de clase se utiliza el decorador @classmethod y por convención se utiliza cls como argumento inicial en lugar de self.Del mismo modo, los métodos de clase utilizan el prefijo cls para referirse a los atributos de la clase.Un método de clase puede modificar el estado de una clase, accediendo a los atributos de dicha clase, aún cuando el método es invocado desde un objeto. En lugar de definirse utilizando self como primer parámetro, se utiliza cls.<jupyter_code>class <Clase>(object):
    ...
    ...
    @classmethod
    def <metodo>(cls, <argumentos>):
        ...
        ...
class PoblacionCensada():
    '''Clase capaz de registrar la cantidad der habitantes de todas sus instancias.'''
    poblacion = 0
    '''Crea censos de población. '''
    
    @classmethod
    def opera_poblacion(cls, operador, cantidad):
        '''Método de clase que registra el número total de población de todas las instancias de la clase.'''
        cls.poblacion = eval(str(cls.poblacion) + operador + str(cantidad))
    
    @classmethod
    def despliega_total(cls):
        '''Método de clase que despliega el atributo de clase cls. población.'''
        return cls.poblacion
    
    def __init__(self, nombre, numero=0):
        print("Se ha creado la población {} con {} habitantes.".format(nombre, numero))
        self.nombre = nombre
        self.poblacion = numero
        self.opera_poblacion('+', self.poblacion)   
    
    def __del__(self):
        self.opera_poblacion('-', self.poblacion) 
edomex = [PoblacionCensada("Tlalnepantla", 600000), PoblacionCensada("Toluca", 1000000),
         PoblacionCensada("Valle de Chalco", 750000), PoblacionCensada('Valle de Bravo', 100000)]
edomex[0].despliega_total()
edomex[0].poblacion
del edomex[1]
edomex[0].despliega_total()<jupyter_output><empty_output><jupyter_text>Métodos estáticos Los métodos estáticos hacen referencia a las instancias y métodos de una clase. Para definir un método estático se utiliza el decorador @staticmethod y no utiliza ningún argumento inicial.Al no utilizar self, los métodos estáticos no pueden interactuar con los atributos y métodos de la instancia.Para referirse a los elementos de la clase, se debe utilizar el nombre de la clase como prefijo.Los métodos estáticos están restringidos en su ámbito, de tal manera que no tienen acceso a los atributos del objeto. Se definen de forma idéntica a una función, sin necesidad de ingresar el parámetro inicial self.<jupyter_code>class <Clase>(object):
    ...
    ...
    @staticmethod
    def <metodo>(<argumentos>):
        ...
        ...
class Servidor:
    '''Clase que emula a un servidor muy básico.'''
    usuarios_activos = set(())
    
    def __init__(self, dominio, lista):
        self.lista_usuarios = lista
        self.dominio = dominio
    
    def conexion(self, usuario):
        '''Conexión de un usuario válido al servidor.'''
        if usuario in self.lista_usuarios:
            self.usuarios_activos.add(usuario)
        else:
            return False
        
    @staticmethod
    def ping(ip):
        '''Regresa el ping a la IP de origen.'''
        return (ip, "ping")    
server = Servidor("demo.pythonista.mx", ["josech", "juan", "mglez", "jklx"])
server.ping("172.16.17.32")
server.lista_usuarios
server.conexion('juan')
server.usuarios_activos<jupyter_output><empty_output><jupyter_text>HerenciaUna de las principales propiedades de las clases es la herencia. Esta propiedad nos permite crear nuevas clases a partir de clases existentes, conservando las propiedades de la clase original y añadiendo otras nuevas. La nueva clase obtenida se conoce como clase derivada, y las clases a partir de las cuales se deriva, clases base. Además, cada clase derivada puede usarse como clase base para obtener una nueva clase derivada Definición de una clase heredada en Python.    class Instrumento:        pass    class Guitarra(Instrumento):        pass    class Bajo(Instrumento):        pass        Cuando creamos una clase derivada a partir de una clase padre y tenemos que la clase derivada proporciona o requiere su propio método \__init__ , este método de la clase derivada debe llamar explícitamente el método \__init__ de la clase base.<jupyter_code>class Animal:
    def __init__(self):
        print("Animal creado")
        
    def quiensoy(self):
        print("Animal")
        
    def comer(self):
        print("Estoy comiendo")
        
class Perro(Animal):
    def __init__(self):
        Animal.__init__(self)
        print("Perro creado")
    
    def quiensoy(self):
        print("Perro")
        
    def ladrar(self):
        print("Woof Woof Woof Woof!")
        
def main():
    perro = Perro()
    perro.quiensoy()
    perro.comer()
    perro.ladrar()
    
if __name__ == '__main__':
    main()<jupyter_output>Animal creado
Perro creado
Perro
Estoy comiendo
Woof Woof Woof Woof!<jupyter_text>Otro ejemplo:<jupyter_code>class Electrodomestico:
    def __init__(self, nombre):
        self.nombre = nombre
        self.estado = False
        
    def encender(self):
        if self.estado == False:
            self.estado = True
            print("Se ha encendido " + self.nombre)
        else:
            print("El electrodomestico ya estaba encendido")
            
    def apagar(self):
        if self.estado:
            self.estado = False
            print("Se ha apagado " + self.nombre)
        else:
            print("El electrodomestico ya estaba apagado")
        
class Celular(Electrodomestico):
    def enviarMensaje(self):
        if self.estado:
            print("Enviando sms")
            
class Televisor(Electrodomestico):
    def cambiarCanal(self):
        if self.estado:
            print("Cambiando de canal")
            
def main():
    cel = Celular("Iphone")
    cel.encender()
    cel.enviarMensaje()
    cel.apagar
    
    tel = Televisor("Televisor LG")
    tel.cambiarCanal()
    tel.apagar()

main()<jupyter_output>Se ha encendido Iphone
Enviando sms
El electrodomestico ya estaba apagado<jupyter_text>Herencia Multiple    class acuatico:        pass    class terrestre:        pass    class anfibio(acuatico, terrestre):        pass<jupyter_code>class Electrodomestico:
    def __init__(self, nombre):
        self.nombre = nombre
        self.estado = False
        
    def encender(self):
        if self.estado == False:
            self.estado = True
            print("Se ha encendido " + self.nombre)
        else:
            print("El electrodomestico ya estaba encendido")
            
    def apagar(self):
        if self.estado:
            self.estado = False
            print("Se ha apagado " + self.nombre)
        else:
            print("El electrodomestico ya estaba apagado")
            
class Telefono:
    def llamar(self):
        print("Llamando")
    
    def colgar(self):
        print("Llamada finalizada")
        
class Celular(Electrodomestico, Telefono):
    def enviarMensaje(self):
        if self.estado:
            print("Enviando sms")
            
class Televisor(Electrodomestico):
    def cambiarCanal(self):
        if self.estado:
            print("Cambiando de canal")
            
def main():
    cel = Celular("Iphone")
    cel.encender()
    cel.enviarMensaje()
    
    cel.llamar()
    cel.colgar()
    
    cel.apagar
    
    tel = Televisor("Televisor LG")
    tel.encender()
    tel.cambiarCanal()
    tel.apagar()

main()<jupyter_output>Se ha encendido Iphone
Enviando sms
Llamando
Llamada finalizada
Se ha encendido Televisor LG
Cambiando de canal
Se ha apagado Televisor LG<jupyter_text>Si las clases Electrodometicos y Telefonos tuvieran un método con el mismo nombre tiene prioridad la herencia que se encuentran a la izquierda es este caso cogería el método de Electrodomestico.  Otro ejemploA veces tiene sentido que una clase derivada herede cualidades de dos o más clases base. Python permite esto con herencia múltiple.<jupyter_code>class Car:
    def __init__(self,wheels=4):
        self.wheels = wheels
        # Diremos que todos los autos, sin importar su motor, tienen cuatro ruedas por defecto.

class Gasoline(Car):
    def __init__(self,engine='Gasoline',tank_cap=20):
        Car.__init__(self)
        self.engine = engine
        self.tank_cap = tank_cap # Representa la capacidad del tanque de combustible en galones.
        self.tank = 0
        
    def refuel(self):
        self.tank = self.tank_cap
        
    
class Electric(Car):
    def __init__(self,engine='Electric',kWh_cap=60):
        Car.__init__(self)
        self.engine = engine
        self.kWh_cap = kWh_cap #Representa la capacidad de la batería en kilovatios-hora.
        self.kWh = 0
    
    def recharge(self):
        self.kWh = self.kWh_cap<jupyter_output><empty_output><jupyter_text>Entonces, ¿qué sucede si tenemos un objeto que comparte propiedades tanto de gasolinas como de electricidad? ¡Podemos crear una clase derivada que hereda de ambos!<jupyter_code>class Hybrid(Gasoline, Electric):
    def __init__(self,engine='Hybrid',tank_cap=11,kWh_cap=5):
        Gasoline.__init__(self,engine,tank_cap)
        Electric.__init__(self,engine,kWh_cap)
        
        
prius = Hybrid()
print(prius.tank)
print(prius.kWh)
prius.recharge()
print(prius.kWh)<jupyter_output>5<jupyter_text>Encapsulación La Encapsulación se consigue en otros lenguajes de programación como Java y C++ utilizando modificadores de acceso que definen si cualquiera puede acceder a esa método o atributo.   En estos lenguajes tenemos los modificaciones: - public -> hace visible los métodos y atributos fuera de la clase. - private -> hace que los métodos y atributos solo sean accesibles por métodos dentro de la clase. En Python no existen los modificadores de acceso.El acceso a una atributo o a los métodos viene determinado determinado por su nombre: si el nombre comienza con dos guiones bajos (y no termina también con dos guiones bajos) se trata de una atributo o método privado, si no es asi estos son públicos.<jupyter_code>class Figura:
    def __init__(self, lados=0, longlado=0, apotema = 0.0):
        self.lado = laodos
        self.long = longlado
        self.__apotema = apotema
        self.__perimetro = self.lado * self.long
        
    def __area(self):
        return (self.__apotema * self.__perimetro) / 2
    
    def imprimir(self):
        pirnt(self.__area())
        
def main():
    triangulo = Figura(2, 3, 1.5)
    print(triangulo.lado)
    print(triangulo.long)
    
    """
    Estas dos lines me lanzara una excpeción, diciendo que los 
    atributos no existe dado que son privados y solo se pueden
    acceder dentro de las clases
    """
    # print(triangulo.__apotema)
    # print(triangulo.__perimetro)
    
    """
    Al igual que los atributos se me presentara una excepción 
    dado que el método solo existe dentro de la clase
    """
    # triangulo.__area()
    
    triangulo.imprimir()<jupyter_output><empty_output><jupyter_text>Usando polimorfismo en Python<jupyter_code>class Parrot:

    def fly(self):
        print("Parrot can fly")
    
    def swim(self):
        print("Parrot can't swim")

class Penguin:

    def fly(self):
        print("Penguin can't fly")
    
    def swim(self):
        print("Penguin can swim")

# common interface
def flying_test(bird):
    bird.fly()

#instantiate objects
blu = Parrot()
peggy = Penguin()

# passing the object
flying_test(blu)
flying_test(peggy)<jupyter_output>Parrot can fly
Penguin can't fly
{'fraction_non_alphanumeric': 0.07644591217422349, 'fraction_numerical': 0.008523741520885399, 'mean_word_length': 3.136028054632706, 'pattern_counts': {'":': 0, '<': 58, '<?xml version=': 0, '>': 71, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 1, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '645676', 'license': 'MIT', 'n_tokens_mistral': 7374, 'n_tokens_neox': 7251, 'n_words': 2477, 'path': 'POO/Apuntes.ipynb', 'repo_name': 'lsantiago/PythonBasico', 'size': '22409'}