Translate

Mostrando las entradas con la etiqueta Python. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Python. Mostrar todas las entradas

lunes, 14 de abril de 2025

PEP 750: ¡Template Strings aceptadas oficialmente en Python!


El lenguaje Python sigue evolucionando con propuestas que buscan mejorar la legibilidad, la seguridad y la flexibilidad del código. Una de las más recientes incorporaciones al lenguaje es la PEP 750, que introduce un nuevo sistema de plantillas de texto: Template Strings.

Aunque Python ya cuenta con varias formas de interpolar texto (como `str.format()` o f-strings), esta propuesta agrega una forma más estructurada y segura de construir cadenas dinámicas, especialmente útil en contextos donde el contenido proviene del usuario o debe ser validado y separado del código de negocio.

La idea es inspirarse en los template literals de lenguajes como JavaScript, pero con una sintaxis propia y flexible.

Características clave:

  • Se introduce un nuevo prefijo de cadena: t"" o t'''...''', al estilo de las f-strings (f"").
  • Las expresiones a interpolar deben ser variables ya definidas (sin expresiones arbitrarias como en las f-strings), lo que reduce riesgos de seguridad.
  • Pueden ser útiles para escenarios como generación de HTML, SQL, etc., donde conviene tener una separación estricta entre datos y estructura.


nombre = "Emanuel"

edad = 30


mensaje = t"Hola, {nombre}. Tenés {edad} años."

print(mensaje)  # Hola, Emanuel. Tenés 30 años.


A diferencia de las f"" strings, no podés hacer expresiones como edad + 1, lo que previene comportamientos inesperados y hace el código más declarativo.

Las Template Strings están diseñadas con seguridad en mente. En lugar de permitir cualquier expresión de Python dentro de la cadena (como en f""), restringen la interpolación a nombres de variables válidos. Esto es ideal en:

  • Aplicaciones web (evitar inyecciones accidentales).
  • Motores de plantillas simplificados.
  • Contextos educativos o de scripting seguro.


La PEP 750 ya fue aceptada y se espera su inclusión oficial en una versión futura de Python (posiblemente Python 3.13 si todo va bien).

Dejo link: https://peps.python.org/pep-0750/

martes, 1 de abril de 2025

Librerías estándar de Python


Python cuenta con una extensa biblioteca estándar que proporciona módulos y funciones listas para usar sin necesidad de instalar paquetes adicionales. A continuación, exploramos algunas de las librerías más útiles y cómo utilizarlas.


Manipulación de Archivos y Directorios


os - Interacción con el sistema operativo

import os

print(os.name)  # Nombre del sistema operativo

print(os.getcwd())  # Directorio actual

os.mkdir("nueva_carpeta")  # Crear una carpeta


shutil - Operaciones de archivos y directorios

import shutil

shutil.copy("archivo.txt", "copia.txt")  # Copiar un archivo

shutil.rmtree("nueva_carpeta")  # Eliminar un directorio


Manejo de Fechas y Tiempos


datetime - Fechas y horas

from datetime import datetime


ahora = datetime.now()

print(ahora.strftime("%Y-%m-%d %H:%M:%S"))  # Formateo de fecha


time - Control de tiempo y pausas

import time


time.sleep(2)  # Pausar ejecución por 2 segundos

print("Dos segundos después...")


Operaciones Matemáticas y Estadísticas


math - Funciones matemáticas


import math


print(math.sqrt(25))  # Raíz cuadrada

print(math.pi)  # Valor de PI


random - Generación de números aleatorios


import random

print(random.randint(1, 100))  # Número aleatorio entre 1 y 100


statistics - Cálculos estadísticos


import statistics


datos = [10, 20, 30, 40, 50]

print(statistics.mean(datos))  # Media

print(statistics.median(datos))  # Mediana


Manejo de Datos en Formato Texto


json - Trabajar con JSON

import json


datos = {"nombre": "Emanuel", "edad": 30}

cadena_json = json.dumps(datos)  # Convertir a JSON

print(json.loads(cadena_json))  # Convertir de JSON a diccionario


csv - Lectura y escritura de archivos CSV

import csv


with open("datos.csv", "w", newline="") as archivo:

    escritor = csv.writer(archivo)

    escritor.writerow(["Nombre", "Edad"])

    escritor.writerow(["Emanuel", 30])


Expresiones Regulares y Manejo de Texto


re - Expresiones regulares


import re


texto = "Correo: usuario@example.com"

patron = r"[\w.-]+@[\w.-]+\.\w+"

resultado = re.search(patron, texto)

print(resultado.group())  # usuario@example.com


Concurrencia y Multiprocesamiento


threading - Programación con hilos


import threading


def tarea():

    print("Ejecutando en un hilo")


hilo = threading.Thread(target=tarea)

hilo.start()


multiprocessing - Procesos en paralelo


import multiprocessing


def tarea():

    print("Ejecutando en un proceso")


proceso = multiprocessing.Process(target=tarea)

proceso.start()


Manejo de Errores y Depuración


logging - Registro de eventos y depuración


import logging


logging.basicConfig(level=logging.INFO)

logging.info("Esto es un mensaje informativo")


La biblioteca estándar de Python proporciona herramientas esenciales para diversas tareas sin necesidad de instalar paquetes adicionales. Conocer y utilizar estos módulos puede hacer que tu código sea más eficiente y organizado.


Importación de módulos y uso de paquetes en Python


Python es un lenguaje que permite organizar el código en módulos y paquetes, facilitando la reutilización y la modularidad. 

Un módulo en Python es simplemente un archivo `.py` que contiene funciones, clases y variables que pueden ser reutilizadas en otros archivos.


Ejemplo de módulo (`mi_modulo.py`):

Definimos una función en el módulo


def saludar(nombre):

    return f"Hola, {nombre}!"


Para usar un módulo en otro script, se puede importar con import:


import mi_modulo

print(mi_modulo.saludar("Emanuel"))  # Salida: Hola, Emanuel!


También se puede importar solo un elemento específico:


from mi_modulo import saludar

print(saludar("Mundo"))


Si se quiere importar todo el contenido de un módulo pero con un alias:


import mi_modulo as mod

print(mod.saludar("Python"))


Un paquete es una carpeta que contiene múltiples módulos y un archivo especial `__init__.py`, el cual indica que la carpeta debe tratarse como un paquete.


Estructura de un paquete:


mi_paquete/

│── __init__.py

│── modulo1.py

│── modulo2.py



Ejemplo de modulo1.py en mi_paquete:


def sumar(a, b):

    return a + b


Ejemplo de modulo2.py en mi_paquete:


def restar(a, b):

    return a - b


Para importar módulos desde un paquete:


from mi_paquete import modulo1, modulo2

print(modulo1.sumar(5, 3))  # Salida: 8

print(modulo2.restar(10, 4))  # Salida: 6


También se puede importar solo una función específica:


from mi_paquete.modulo1 import sumar

print(sumar(7, 2))


Importación relativa vs. absoluta


- Importación absoluta: Se usa la ruta completa desde el paquete raíz.

 

 from mi_paquete.modulo1 import sumar


- Importación relativa: Se usa `.` o `..` para referirse a módulos dentro del mismo paquete.

 

 from .modulo1 import sumar  # Desde el mismo paquete

 from ..otro_paquete.modulo3 import dividir  # Desde un paquete hermano


Python incluye una gran cantidad de módulos en su biblioteca estándar, que pueden ser importados directamente:


import math

print(math.sqrt(25))  # Salida: 5.0


Para instalar e importar paquetes de terceros (ejemplo: requests):


$ pip install requests



import requests

response = requests.get("https://api.github.com")

print(response.status_code)


El uso de módulos y paquetes en Python permite mantener el código organizado, reutilizable y fácil de mantener. Con las diferentes opciones de importación, puedes estructurar tus proyectos de manera eficiente.

martes, 25 de marzo de 2025

Sobrecarga de Métodos en Python


La sobrecarga de métodos (method overloading) ocurre cuando una clase define múltiples métodos con el mismo nombre pero diferentes parámetros. Sin embargo, Python no admite sobrecarga de métodos de la misma manera que lenguajes como Java o C++. En Python, si se define un método con el mismo nombre más de una vez, la última definición sobrescribe a las anteriores.

Ejemplo de Sobrecarga (Que No Funciona en Python)


class Calculadora:

    def sumar(self, a, b):

        return a + b


    def sumar(self, a, b, c):  # Sobrescribe el método anterior

        return a + b + c


calc = Calculadora()

print(calc.sumar(2, 3))  # Error: Argumentos incorrectos


El método sumar(a, b) se sobrescribe con sumar(a, b, c), por lo que llamar sumar(2, 3) genera un error.


Dado que Python no permite sobrecarga nativa, podemos lograr un comportamiento similar con valores por defecto, *args y @singledispatch.


Podemos definir parámetros opcionales para manejar diferentes cantidades de argumentos.


class Calculadora:

    def sumar(self, a, b, c=0):  # c es opcional

        return a + b + c


calc = Calculadora()

print(calc.sumar(2, 3))     # 5  (usa c=0 por defecto)

print(calc.sumar(2, 3, 4))  # 9  (usa c=4)


Esto imita la sobrecarga permitiendo diferentes cantidades de argumentos.

Si queremos permitir una cantidad dinámica de argumentos, podemos usar *args.


class Calculadora:

    def sumar(self, *args):

        return sum(args)  # Suma todos los argumentos recibidos


calc = Calculadora()

print(calc.sumar(2, 3))       # 5

print(calc.sumar(2, 3, 4, 5)) # 14


Aquí *args permite cualquier número de argumentos.

El decorador functools.singledispatch permite definir funciones con el mismo nombre pero con diferentes tipos de argumento.


from functools import singledispatch


@singledispatch

def procesar(valor):

    raise NotImplementedError("Tipo no soportado")


@procesar.register(int)

def _(valor):

    return f"Procesando número: {valor}"


@procesar.register(str)

def _(valor):

    return f"Procesando texto: {valor.upper()}"


print(procesar(10))     # Procesando número: 10

print(procesar("hola")) # Procesando texto: HOLA


Este enfoque permite sobrecargar la función dependiendo del tipo de dato del argumento.


domingo, 23 de marzo de 2025

Programación Orientada a Objetos en Python parte 5


Seguimos con poo y python.

La clase object en Python es la superclase base de todas las clases. Esto significa que cualquier clase que crees hereda, directa o indirectamente, de object. 

object es la raíz de la jerarquía de clases en Python. Si defines una clase sin especificar una superclase, automáticamente hereda de object.


class MiClase:  # Equivalente a class MiClase(object)

    pass


print(issubclass(MiClase, object))  # True


En versiones modernas de Python (desde Python 3), todas las clases heredan implícitamente de object.

Al heredar de object, las clases obtienen algunos métodos fundamentales:


__str__ y __repr__ : Estos métodos definen cómo se representa una instancia como cadena:


class Ejemplo:

    def __str__(self):

        return "Ejemplo como str"


    def __repr__(self):

        return "Ejemplo()"


obj = Ejemplo()

print(str(obj))   # "Ejemplo como str"

print(repr(obj))  # "Ejemplo()"


__eq__ y __hash__ : Permiten comparar objetos y usarlos en estructuras como set o dict.


class Persona:

    def __init__(self, nombre):

        self.nombre = nombre


    def __eq__(self, otro):

        return isinstance(otro, Persona) and self.nombre == otro.nombre


    def __hash__(self):

        return hash(self.nombre)


p1 = Persona("Alice")

p2 = Persona("Alice")

print(p1 == p2)  # True (porque definimos __eq__)

print(hash(p1) == hash(p2))  # True (porque __hash__ usa el nombre)


__class__ : Permite acceder a la clase de un objeto.


print(obj.__class__)  # <class '__main__.Ejemplo'>


Si bien no es necesario, se puede hacer explícita la herencia:


class MiClase(object):  # En Python 3 esto es redundante

    pass


Esto era más importante en Python 2, donde existían diferencias entre clases clásicas y clases de nuevo estilo.

La sobrescritura de métodos (method overriding) ocurre cuando una subclase redefine un método de su superclase con la misma firma (mismo nombre y parámetros). Esto permite modificar o extender el comportamiento heredado.

Cuando una subclase sobrescribe un método de su superclase, la nueva versión reemplaza a la anterior.


class Animal:

    def hacer_sonido(self):

        return "Sonido genérico"


class Perro(Animal):

    def hacer_sonido(self):  # Sobrescribe el método

        return "Guau Guau!"


animal = Animal()

perro = Perro()


print(animal.hacer_sonido())  # Sonido genérico

print(perro.hacer_sonido())   # Guau Guau!


Aquí, Perro redefine hacer_sonido(), cambiando el comportamiento original.

Para reutilizar la implementación de la superclase y agregar nueva funcionalidad, usamos super().


class Vehiculo:

    def describir(self):

        return "Soy un vehículo."


class Coche(Vehiculo):

    def describir(self):

        return super().describir() + " Soy un coche."


c = Coche()

print(c.describir())  # Soy un vehículo. Soy un coche.


super().describir() llama al método original en Vehiculo, evitando reescribir código innecesario.

Podemos sobrescribir el constructor (__init__) de la superclase, llamando a super() para inicializar atributos.


class Persona:

    def __init__(self, nombre):

        self.nombre = nombre


    def presentarse(self):

        return f"Hola, soy {self.nombre}."


class Estudiante(Persona):

    def __init__(self, nombre, universidad):

        super().__init__(nombre)  # Llamamos al __init__ de Persona

        self.universidad = universidad


    def presentarse(self):

        return super().presentarse() + f" Estudio en {self.universidad}."


e = Estudiante("Carlos", "MIT")

print(e.presentarse())  # Hola, soy Carlos. Estudio en MIT.


Aquí, Estudiante extiende __init__ y presentarse(), reutilizando la lógica de Persona.


Podemos redefinir métodos especiales para personalizar el comportamiento de nuestras clases.


class Libro:

    def __init__(self, titulo, autor):

        self.titulo = titulo

        self.autor = autor


    def __str__(self):  # Representación amigable para humanos

        return f"Libro: {self.titulo} de {self.autor}"


    def __repr__(self):  # Representación para depuración

        return f"Libro({repr(self.titulo)}, {repr(self.autor)})"


libro = Libro("1984", "George Orwell")

print(str(libro))   # Libro: 1984 de George Orwell

print(repr(libro))  # Libro('1984', 'George Orwell')


En lenguajes como Java o C++, upcasting y downcasting son técnicas que permiten tratar objetos de una subclase como si fueran de su superclase (upcasting) o convertir un objeto de la superclase en su subclase específica (downcasting).

Python NO tiene upcasting ni downcasting en el sentido estricto porque su sistema de tipos es dinámico y no requiere conversiones explícitas. Sin embargo, se pueden imitar estos conceptos utilizando la herencia y el manejo de instancias.

En upcasting, un objeto de una subclase se trata como si fuera de su superclase. Esto es natural en Python, ya que una subclase hereda automáticamente los métodos y atributos de su superclase.


class Animal:

    def hacer_sonido(self):

        return "Sonido genérico"


class Perro(Animal):

    def hacer_sonido(self):

        return "Guau Guau!"


# Upcasting: Tratamos un Perro como un Animal

animal: Animal = Perro()  # No es necesario hacer conversión

print(animal.hacer_sonido())  # Guau Guau!


En Python, no es necesario realizar una conversión explícita para tratar un objeto de una subclase como su superclase. Simplemente se puede asignar una instancia de la subclase a una variable del tipo de la superclase.

El downcasting ocurre cuando convertimos un objeto de una superclase a su subclase específica. En Python, esto no es seguro y debe hacerse con validaciones, ya que un objeto de la superclase puede no tener los métodos de la subclase.


class Animal:

    def hacer_sonido(self):

        return "Sonido genérico"


class Perro(Animal):

    def hacer_sonido(self):

        return "Guau Guau!"


    def correr(self):

        return "El perro está corriendo."


# Upcasting: Perro como Animal

animal: Animal = Perro()


# Downcasting: Verificamos si el objeto es realmente un Perro

if isinstance(animal, Perro):

    perro: Perro = animal  # No hay conversión explícita, solo reasignación

    print(perro.correr())  # El perro está corriendo.

else:

    print("No se puede hacer downcasting.")


Python no impide el downcasting, pero no lo recomienda porque se basa en la verificación de tipo en tiempo de ejecución (isinstance()). Si intentamos acceder a un método de Perro en un objeto que realmente es Animal, fallará.

En Python, las clases abstractas y los métodos abstractos permiten definir estructuras base que las subclases deben implementar. Se logran con el módulo abc (Abstract Base Class).

Una clase abstracta es aquella que no puede instanciarse directamente y sirve como plantilla para otras clases.

  • Se define usando ABC del módulo abc.  
  • Si una clase tiene al menos un método abstracto, debe ser abstracta.  

Ejemplo:


from abc import ABC, abstractmethod


class Animal(ABC):  # Clase abstracta

    @abstractmethod

    def hacer_sonido(self):

        pass  # No tiene implementación


# Error si intentamos instanciar

# a = Animal()  # TypeError: Can't instantiate abstract class Animal


Animal no se puede instanciar porque tiene métodos abstractos.

Un método abstracto es aquel que debe ser implementado en las subclases.


Ejemplo:


class Perro(Animal):  # Subclase concreta

    def hacer_sonido(self):

        return "Guau Guau!"  # Implementación obligatoria


p = Perro()

print(p.hacer_sonido())  # Guau Guau!


Perro hereda de Animal y debe implementar hacer_sonido(), o Python lanzará un error.

En Python, un método abstracto puede tener una implementación base:


class Ave(ABC):

    @abstractmethod

    def volar(self):

        print("Algunas aves pueden volar")  # Implementación opcional


class Aguila(Ave):

    def volar(self):

        super().volar()

        print("El águila vuela alto")


a = Aguila()

a.volar()


Las subclases pueden llamar super().volar() para reutilizar la implementación base.

También podemos definir propiedades abstractas con @property:


class Figura(ABC):

    @property

    @abstractmethod

    def area(self):

        pass  # Obliga a definir `area` en las subclases


class Cuadrado(Figura):

    def __init__(self, lado):

        self._lado = lado


    @property

    def area(self):

        return self._lado ** 2


c = Cuadrado(4)

print(c.area)  # 16


La subclase debe implementar la propiedad area, o Python lanzará un error.

El polimorfismo es un concepto clave en la Programación Orientada a Objetos (POO) que permite que diferentes clases tengan métodos con el mismo nombre, pero con comportamientos distintos. En Python, el polimorfismo se aplica de manera dinámica y es muy flexible.

Podemos definir métodos con el mismo nombre en diferentes clases y usarlos sin importar el tipo de objeto.


class Perro:

    def hacer_sonido(self):

        return "Guau Guau!"


class Gato:

    def hacer_sonido(self):

        return "Miau Miau!"


# Función polimórfica

def sonido_animal(animal):

    print(animal.hacer_sonido())


# Ambas clases tienen el método `hacer_sonido`, pero con comportamientos diferentes

perro = Perro()

gato = Gato()


sonido_animal(perro)  # Guau Guau!

sonido_animal(gato)   # Miau Miau!


Python permite usar hacer_sonido() sin importar el tipo de objeto porque ambos lo implementan.

Si una clase base define un método, las subclases pueden sobrescribirlo con su propia implementación.


class Ave:

    def volar(self):

        return "Algunas aves pueden volar"


class Aguila(Ave):

    def volar(self):

        return "El águila vuela alto y rápido"


class Pinguino(Ave):

    def volar(self):

        return "El pingüino no puede volar"


# Lista de objetos polimórficos

aves = [Aguila(), Pinguino()]


for ave in aves:

    print(ave.volar())


# Salida:

# El águila vuela alto y rápido

# El pingüino no puede volar


volar() tiene diferentes comportamientos según la subclase.

Podemos forzar a las subclases a implementar métodos específicos usando clases abstractas (ABC).


from abc import ABC, abstractmethod


class Figura(ABC):

    @abstractmethod

    def area(self):

        pass


class Cuadrado(Figura):

    def __init__(self, lado):

        self.lado = lado


    def area(self):

        return self.lado ** 2


class Circulo(Figura):

    def __init__(self, radio):

        self.radio = radio


    def area(self):

        return 3.14 * self.radio ** 2


# Lista polimórfica

figuras = [Cuadrado(4), Circulo(3)]


for figura in figuras:

    print(figura.area())  # Python llama al método `area()` correspondiente


Gracias al polimorfismo, podemos iterar sobre figuras sin preocuparnos del tipo.

Python tambien permite redefinir operadores como +,*,== mediante métodos especiales (dunder methods).


class Punto:

    def __init__(self, x, y):

        self.x = x

        self.y = y


    def __add__(self, otro):

        return Punto(self.x + otro.x, self.y + otro.y)


    def __str__(self):

        return f"({self.x}, {self.y})"


p1 = Punto(2, 3)

p2 = Punto(5, 7)


print(p1 + p2)  # (7, 10)

+ se comporta diferente gracias a la sobrecarga de operadores.


jueves, 20 de marzo de 2025

Programación Orientada a Objetos en Python parte 4


Seguimos con poo y python.

Una vez que nos familiarizamos con los conceptos básicos de POO podemos ahora enfocarnos en herramientas que nos permiten aprovechar mejor sus beneficios, veremos a continuación los pilares fundamentales sobre los que se basa la Programación Orientada a Objetos.

La herencia es una herramienta que fomenta la reusabilidad del código. Permite a una clase poder reutilizar (o heredar) compartamientos y/o atributos de otra clase, denominada superclase o clase padre. Muchas veces nos encontramos con escenarios en donde tenemos implementada ya en cierta clase, atributos o métodos que necesitamos luego utilizar en otra clase. Por ejemplo supongamos que contamos con un método registrar en la clase Venta y ahora nos encontramos desarrollando una clase VentaEnLinea, la cual tiene comportamientos y atributos similares a la anterior, pero se diferencia en algunos atributos y comportamientos. Aplicando herencia podremos reutilizar lo definido en la clase anterior e inclusive modificar comportamientos definidos para solucionar nuestro problema. 

La herencia es un proceso mediante el cual se puede crear una clase hija que hereda de una clase padre, compartiendo sus métodos y atributos. Además de ello, una clase hija puede sobreescribir los métodos o atributos, o incluso definir unos nuevos.

Se puede crear una clase hija con tan solo pasar como parámetro la clase de la que queremos heredar. En el siguiente ejemplo vemos como se puede usar la herencia en Python, con la clase Perro que hereda de Animal. Así de fácil.


# Definimos una clase padre

class Animal:

    pass


# Creamos una clase hija que hereda de la padre

class Perro(Animal):

    pass


De hecho podemos ver como efectivamente la clase Perro es la hija de Animal usando __bases__


print(Perro.__bases__)

# (<class '__main__.Animal'>,)


De manera similar podemos ver que clases descienden de una en concreto con __subclasses__.


print(Animal.__subclasses__())

# [<class '__main__.Perro'>]


¿Y para que queremos la herencia? Dado que una clase hija hereda los atributos y métodos de la padre, nos puede ser muy útil cuando tengamos clases que se parecen entre sí pero tienen ciertas particularidades. En este caso en vez de definir un montón de clases para cada animal, podemos tomar los elementos comunes y crear una clase Animal de la que hereden el resto, respetando por tanto la filosofía DRY. Realizar estas abstracciones y buscar el denominador común para definir una clase de la que hereden las demás, es una tarea de lo más compleja en el mundo de la programación.

Para saber más: El principio DRY (Don't Repeat Yourself) es muy aplicado en el mundo de la programación y consiste en no repetir código de manera innecesaria. Cuanto más código duplicado exista, más difícil será de modificar y más fácil será crear inconsistencias. Las clases y la herencia a no repetir código.

Continuemos con nuestro ejemplo de perros y animales. Vamos a definir una clase padre Animal que tendrá todos los atributos y métodos genéricos que los animales pueden tener. Esta tarea de buscar el denominador común es muy importante en programación. Veamos los atributos:

Tenemos la especie ya que todos los animales pertenecen a una.

Y la edad, ya que todo ser vivo nace, crece, se reproduce y muere.

Y los métodos o funcionalidades:

Tendremos el método hablar, que cada animal implementará de una forma. Los perros ladran, las abejas zumban y los caballos relinchan.

Un método moverse. Unos animales lo harán caminando, otros volando.

Y por último un método descríbeme que será común.

Definimos la clase padre, con una serie de atributos comunes para todos los animales como hemos indicado.


class Animal:

    def __init__(self, especie, edad):

        self.especie = especie

        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 un Animal del tipo", type(self).__name__)


Tenemos ya por lo tanto una clase genérica Animal, que generaliza las características y funcionalidades que todo animal puede tener. Ahora creamos una clase Perro que hereda del Animal. Como primer ejemplo vamos a crear una clase vacía, para ver como los métodos y atributos son heredados por defecto.


# Perro hereda de Animal

class Perro(Animal):

    pass


mi_perro = Perro('mamífero', 10)

mi_perro.describeme()

# Soy un Animal del tipo Perro


Con tan solo un par de líneas de código, hemos creado una clase nueva que tiene todo el contenido que la clase padre tiene, pero aquí viene lo que es de verdad interesante. Vamos a crear varios animales concretos y sobreescrbir algunos de los métodos que habían sido definidos en la clase Animal, como el hablar o el moverse, ya que cada animal se comporta de una manera distinta.

Podemos incluso crear nuevos métodos que se añadirán a los ya heredados, como en el caso de la Abeja con picar().


class Perro(Animal):

    def hablar(self):

        print("Guau!")

    def moverse(self):

        print("Caminando con 4 patas")


class Vaca(Animal):

    def hablar(self):

        print("Muuu!")

    def moverse(self):

        print("Caminando con 4 patas")


class Abeja(Animal):

    def hablar(self):

        print("Bzzzz!")

    def moverse(self):

        print("Volando")


    # Nuevo método

    def picar(self):

        print("Picar!")


Por lo tanto ya podemos crear nuestros objetos de esos animales y hacer uso de sus métodos que podrían clasificarse en tres:

  • 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()

      

mi_perro = Perro('mamífero', 10)

mi_vaca = Vaca('mamífero', 23)

mi_abeja = Abeja('insecto', 1)


mi_perro.hablar()

mi_vaca.hablar()

# Guau!

# Muuu!


mi_vaca.describeme()

mi_abeja.describeme()

# Soy un Animal del tipo Vaca

# Soy un Animal del tipo Abeja


mi_abeja.picar()

# Picar!


La función super() nos permite acceder a los métodos de la clase padre desde una de sus hijas. Volvamos al ejemplo de Animal y Perro.


class Animal:

    def __init__(self, especie, edad):

        self.especie = especie

        self.edad = edad        


    def hablar(self):

        pass


    def moverse(self):

        pass


    def describeme(self):

        print("Soy un Animal del tipo", type(self).__name__)


Tal vez queramos que nuestro Perro tenga un parámetro extra en el constructor, como podría ser el dueño. 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 la especie y edad, y sólo asignar la variable nueva manualmente.


class Perro(Animal):

    def __init__(self, especie, edad, dueño):

        # Alternativa 1

        # self.especie = especie

        # self.edad = edad

        # self.dueño = dueño


        # Alternativa 2

        super().__init__(especie, edad)

        self.dueño = dueño


mi_perro = Perro('mamífero', 7, 'Luis')

mi_perro.especie

mi_perro.edad

mi_perro.dueño


En Python es posible realizar herencia múltiple. En otros posts hemos visto como se podía crear una clase padre que heredaba de una clase hija, pudiendo hacer uso de sus métodos y atributos. La herencia múltiple es similar, pero una clase hereda de varias clases padre en vez de una sola.

Veamos un ejemplo. Por un lado tenemos dos clases Clase1 y Clase2, y por otro tenemos la Clase3 que hereda de las dos anteriores. Por lo tanto, heredará todos los métodos y atributos de ambas.


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. 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 '__main__.Clase3'>, <class '__main__.Clase1'>, <class '__main__.Clase2'>, <class 'object'>)


Una curiosidad es que al final del todo vemos la clase object. Aunque pueda parecer raro, es correcto ya que en realidad todas las clases en Python heredan de una clase genérica object, aunque no lo especifiquemos explícitamente.

Y como último ejemplo,…el cielo es el límite. Podemos tener una clase heredando de otras tres. Fíjate en que el MRO depende del orden en el que las clases son pasadas: 1, 3, 2.


class Clase1:

    pass

class Clase2:

    pass

class Clase3:

    pass

class Clase4(Clase1, Clase3, Clase2):

    pass

print(Clase4.__mro__)

# (<class '__main__.Clase4'>, <class '__main__.Clase1'>, <class '__main__.Clase3'>, <class '__main__.Clase2'>, <class 'object'>)


miércoles, 12 de marzo de 2025

Programación Orientada a Objetos en Python parte 3



Seguimos con poo y python. 

En Python, el parámetro `self` se usa en los métodos de una clase para hacer referencia a la instancia actual del objeto. 

Cada vez que se llama a un método de una instancia de una clase, Python pasa automáticamente la instancia como primer argumento. El nombre convencional para este argumento es self, aunque puedes llamarlo de otra manera (aunque no se recomienda porque es una convención establecida).

Veamos un ejemplo:


class Persona:

    def __init__(self, nombre, edad):

        self.nombre = nombre  # `self.nombre` es un atributo de la instancia

        self.edad = edad      # `self.edad` también es un atributo de la instancia


    def saludar(self):

        print(f"Hola, mi nombre es {self.nombre} y tengo {self.edad} años.")


# Crear una instancia de la clase

p1 = Persona("Carlos", 30)


# Llamar al método saludar

p1.saludar()


  • self.nombre = nombre: Guarda el valor del parámetro nombre en el atributo nombre de la instancia.
  • def saludar(self): El método recibe self, lo que le permite acceder a los atributos nombre y edad de la instancia p1.
  • Cuando se llama a p1.saludar(), Python en realidad ejecuta Persona.saludar(p1), pasando la instancia p1 como primer argumento.

self es obligatorio en métodos de instancia porque permite acceder a los atributos y otros métodos de la instancia. Si se omite, se generará un error:


class Prueba:

    def mensaje():

        print("Este método no tiene self.")


p = Prueba()

p.mensaje()  # TypeError: mensaje() takes 0 positional arguments but 1 was given


Aquí, Python intenta pasar la instancia p automáticamente, pero como el método mensaje() no tiene self, se genera un error.


El parámetro self también se usa en métodos especiales como __str__, __repr__, __eq__, etc.:


class Auto:

    def __init__(self, marca):

        self.marca = marca


    def __str__(self):

        return f"Auto de marca {self.marca}"


a = Auto("Toyota")

print(a)  # Auto de marca Toyota


En Python, un constructor es un método especial que se ejecuta automáticamente cuando se crea una nueva instancia de una clase. Se define con el método __init__.

__init__ es el constructor de una clase en Python. Se usa para inicializar los atributos del objeto cuando se crea una instancia.


class Persona:

    def __init__(self, nombre, edad):

        self.nombre = nombre  # Atributo de la instancia

        self.edad = edad      # Atributo de la instancia


    def presentarse(self):

        print(f"Hola, soy {self.nombre} y tengo {self.edad} años.")


# Crear una instancia

p1 = Persona("Carlos", 30)


# Llamar a un método

p1.presentarse()


  • __init__ recibe self, que es la instancia del objeto.
  • self.nombre = nombre almacena el valor en el objeto.
  • Cuando se crea una instancia con Persona("Carlos", 30), Python ejecuta automáticamente __init__, asignando "Carlos" a self.nombre y 30 a self.edad.


Se pueden definir valores predeterminados en los parámetros del constructor:


class Coche:

    def __init__(self, marca="Toyota", modelo="Corolla"):

        self.marca = marca

        self.modelo = modelo


c1 = Coche()  # Usa los valores por defecto

c2 = Coche("Ford", "Focus")  # Pasa valores específicos


print(c1.marca, c1.modelo)  # Toyota Corolla

print(c2.marca, c2.modelo)  # Ford Focus


También es posible definir un constructor sin argumentos (además de `self`):


class Ejemplo:

    def __init__(self):

        print("Se ha creado una nueva instancia.")


obj = Ejemplo()  # Se imprimirá el mensaje


El constructor puede contener lógica para validar datos o modificar valores antes de asignarlos:



class Usuario:

    def __init__(self, nombre, edad):

        self.nombre = nombre

        self.edad = max(0, edad)  # Evita edades negativas


u1 = Usuario("Ana", -5)

print(u1.edad)  # 0


Para poder utilizar objetos debemos definir una variable de referencia y luego instanciar dicho objeto, como vimos anteriormente:

c1 = Coche()  # Usa los valores por defecto

c2 = Coche("Ford", "Focus")  # Pasa valores específicos


Una vez definido un objeto podemos interactuar con él invocando sus atributos (lo cual no es recomendado porque rompemos el principio del encapsulamiento) públicos e interactuando con sus métodos:

c1.marca = "Ford"

c1.modelo = ”Fiesta”

lunes, 3 de marzo de 2025

Programación Orientada a Objetos en Python parte 2


Ya vimos lo complejo que es crear software y como la programación orientada a objetos nos ayuda.

Ahora veamos como se aplican en Python, empecemos con las reglas de Nomenclatura.

En Python existen convenciones de nomenclatura recomendadas por la PEP 8, que es la guía de estilo oficial del lenguaje. Las reglas principales para definir clases, métodos y atributos:

Nombres de Clases  

- Se usa PascalCase (también conocido como **CamelCase** con la primera letra en mayúscula).  

- Cada palabra comienza con mayúscula, sin guiones bajos.  

Por ejemplo: 

  class MiClase:

      pass


Nombres de Métodos y Atributos Publicos  

- Se usa snake_case (minúsculas separadas por guiones bajos).  

Por ejemplo:

  class Persona:

      def obtener_nombre(self):

          return "Juan"


Nombres de Métodos y Atributos Privados

- Se usa un guion bajo inicial (_) para indicar que es un atributo/método de uso interno (convención, no es privado en sentido estricto).  

Por ejemplo:

  class Persona:

      def _calcular_edad(self):

          return 30


Atributos y Métodos Realmente Privados  

- Se usa doble guion bajo (`__`) para evitar colisiones de nombres en clases hijas (_name mangling_).  

Por ejemplo:

  class Persona:

      def __metodo_secreto(self):

          print("Este método es realmente privado")


Atributos y Métodos Estáticos o de Clase  

- Métodos de clase (@classmethod) y atributos de clase siguen la convención snake_case.  

- Métodos estáticos (@staticmethod) también siguen snake_case.

Por ejemplo:

  class Utilidades:

      @classmethod

      def metodo_de_clase(cls):

          pass


      @staticmethod

      def metodo_estatico():

          pass


Constantes

- Se usan mayúsculas con guiones bajos.  

Por ejemplo:

  class Config:

      MAX_INTENTOS = 5


Definiendo clases

Vista ya la parte teórica, vamos a ver como podemos hacer uso de la programación orientada a objetos en Python. Lo primero es crear una clase, para ello usaremos el ejemplo de perro.


# Creando una clase vacía

class Perro:

    pass


Se trata de una clase vacía y sin mucha utilidad práctica, pero es la mínima clase que podemos crear. Nótese el uso del pass que no hace realmente nada, pero daría un error si después de los : no tenemos contenido.

Ahora que tenemos la clase, podemos crear un objeto de la misma. Podemos hacerlo como si de una variable normal se tratase. Nombre de la variable igual a la clase con (). Dentro de los paréntesis irían los parámetros de entrada si los hubiera.


# Creamos un objeto de la clase perro

mi_perro = Perro()


Definiendo atributos

A continuación vamos a añadir algunos atributos a nuestra clase. Antes de nada es importante distinguir que existen dos tipos de atributos:

Atributos de instancia: Pertenecen a la instancia de la clase o al objeto. Son atributos particulares de cada instancia, en nuestro caso de cada perro.

Atributos de clase: Se trata de atributos que pertenecen a la clase, por lo tanto serán comunes para todos los objetos.

Empecemos creando un par de atributos de instancia para nuestro perro, el nombre y la raza. Para ello creamos un método __init__ que será llamado automáticamente cuando creemos un objeto. Se trata del constructor.


class Perro:

    

    # El método __init__ es llamado al crear el objeto

    def __init__(self, nombre, raza):

        print(f"Creando perro {nombre}, {raza}")


        # Atributos de instancia

        self.nombre = nombre

        self.raza = raza


Ahora que hemos definido el método init con dos parámetros de entrada, podemos crear el objeto pasando el valor de los atributos. Usando type() podemos ver como efectivamente el objeto es de la clase Perro.


mi_perro = Perro("Toby", "Bulldog")

print(type(mi_perro))

# Creando perro Toby, Bulldog

# <class '__main__.Perro'>


Seguramente te hayas fijado en el self que se pasa como parámetro de entrada del método. Es una variable que representa la instancia de la clase, y deberá estar siempre ahí.

El uso de __init__ y el doble __ no es una coincidencia. Cuando veas un método con esa forma, significa que está reservado para un uso especial del lenguaje. En este caso sería lo que se conoce como constructor. 

Por último, podemos acceder a los atributos usando el objeto y . (el punto)


print(mi_perro.nombre) # Toby

print(mi_perro.raza)   # Bulldog


Hasta ahora hemos definido atributos de instancia, ya que son atributos que pertenecen a cada perro concreto. Ahora vamos a definir un atributo de clase, que será común para todos los perros. Por ejemplo, la especie de los perros es algo común para todos los objetos Perro.


class Perro:

    # Atributo de clase

    especie = 'mamífero'


    # El método __init__ es llamado al crear el objeto

    def __init__(self, nombre, raza):

        print(f"Creando perro {nombre}, {raza}")


        # Atributos de instancia

        self.nombre = nombre

        self.raza = raza


Dado que es un atributo de clase, no es necesario crear un objeto para acceder al atributos. Podemos hacer lo siguiente.


print(Perro.especie)

# mamífero


Se puede acceder también al atributo de clase desde el objeto.


mi_perro = Perro("Toby", "Bulldog")

mi_perro.especie

# 'mamífero'


De esta manera, todos los objetos que se creen de la clase perro compartirán ese atributo de clase, ya que pertenecen a la misma.


Definiendo métodos

En realidad cuando usamos __init__ anteriormente ya estábamos definiendo un método, solo que uno especial. A continuación vamos a ver como definir métodos que le den alguna funcionalidad interesante a nuestra clase, siguiendo con el ejemplo de perro.

Vamos a codificar dos métodos, ladrar y caminar. El primero no recibirá ningún parámetro y el segundo recibirá el número de pasos que queremos andar. Como hemos indicado anteriormente self hace referencia a la instancia de la clase. Se puede definir un método con def y el nombre, y entre () los parámetros de entrada que recibe, donde siempre tendrá que estar self el primero.


class Perro:

    # Atributo de clase

    especie = 'mamífero'


    # El método __init__ es llamado al crear el objeto

    def __init__(self, nombre, raza):

        print(f"Creando perro {nombre}, {raza}")


        # Atributos de instancia

        self.nombre = nombre

        self.raza = raza


    def ladra(self):

        print("Guau")


    def camina(self, pasos):

        print(f"Caminando {pasos} pasos")


Por lo tanto si creamos un objeto mi_perro, podremos hacer uso de sus métodos llamándolos con . y el nombre del método. Como si de una función se tratase, pueden recibir y devolver argumentos.


mi_perro = Perro("Toby", "Bulldog")

mi_perro.ladra()

mi_perro.camina(10)


# Creando perro Toby, Bulldog

# Guau

# Caminando 10 pasos


Métodos en Python: instancia, clase y estáticos

Es posible crear diferentes tipos de métodos:

  • Lo métodos de instancia “normales” que ya hemos visto como metodo()
  • Métodos de clase usando el decorador @classmethod
  • Y métodos estáticos usando el decorador @staticmethod

En la siguiente clase tenemos un ejemplo donde definimos los tres tipos de métodos.


class Clase:

    def metodo(self):

        return 'Método normal', self


    @classmethod

    def metododeclase(cls):

        return 'Método de clase', cls


    @staticmethod

    def metodoestatico():

        return "Método estático"


Veamos su comportamiento en detalle uno por uno.


Métodos de instancia

Los métodos de instancia son los métodos normales, de toda la vida, que hemos visto anteriormente. Reciben como parámetro de entrada self que hace referencia a la instancia que llama al método. También pueden recibir otros argumentos como entrada.

Para saber más: El uso de "self" es totalmente arbitrario. Se trata de una convención acordada por los usuarios de Python, usada para referirse a la instancia que llama al método, pero podría ser cualquier otro nombre. Lo mismo ocurre con "cls", que veremos a continuación.


class Clase:

    def metodo(self, arg1, arg2):

        return 'Método normal', self


Y como ya sabemos, una vez creado un objeto pueden ser llamados.


mi_clase = Clase()

mi_clase.metodo("a", "b")

# ('Método normal', <__main__.Clase at 0x10b9daa90>)


En vista a esto, los métodos de instancia:

  •  Pueden acceder y modificar los atributos del objeto.
  •  Pueden acceder a otros métodos.
  •   Dado que desde el objeto self se puede acceder a la clase con ` self.class`, también pueden modificar el estado de la clase


Métodos de clase (classmethod)

A diferencia de los métodos de instancia, los métodos de clase reciben como argumento cls, que hace referencia a la clase. Por lo tanto, pueden acceder a la clase pero no a la instancia.


class Clase:

    @classmethod

    def metododeclase(cls):

        return 'Método de clase', cls


Se pueden llamar sobre la clase.


Clase.metododeclase()

# ('Método de clase', __main__.Clase)


Pero también se pueden llamar sobre el objeto.


mi_clase.metododeclase()

# ('Método de clase', __main__.Clase)


Por lo tanto, los métodos de clase:

  • No pueden acceder a los atributos de la instancia.
  • Pero si pueden modificar los atributos de la clase.
  • Métodos estáticos (staticmethod)

Por último, los métodos estáticos se pueden definir con el decorador @staticmethod y no aceptan como parámetro ni la instancia ni la clase. Es por ello por lo que no pueden modificar el estado ni de la clase ni de la instancia. Pero por supuesto pueden aceptar parámetros de entrada.


class Clase:

    @staticmethod

    def metodoestatico():

        return "Método estático"


mi_clase = Clase()

Clase.metodoestatico()

mi_clase.metodoestatico()


# 'Método estático'

# 'Método estático'


Por lo tanto el uso de los métodos estáticos pueden resultar útil para indicar que un método no modificará el estado de la instancia ni de la clase. Es cierto que se podría hacer lo mismo con un método de instancia por ejemplo, pero a veces resulta importante indicar de alguna manera estas peculiaridades, evitando así futuros problemas y malentendidos.

En otras palabras, los métodos estáticos se podrían ver como funciones normales, con la salvedad de que van ligadas a una clase concreta.

viernes, 21 de febrero de 2025

Programación Orientada a Objetos en Python


Nos encontramos hoy en día en un mundo en donde debemos realizar software que cumpla al menos con las siguientes características, las cuales al menos hacen que ésta tarea sea bastante compleja:

    • El Dominio del Problema: muchas veces las herramientas y tecnologías a utilizar dependen del problema que queremos resolver, ésto quiere decir que en base al dominio del mismo se definen requisitos, características y modelos que nosotros como desarrolladores debemos plasmar; los mismos hacen que sea difícil diseñar soluciones adaptables a varios escenarios, ya que dichos requisitos son muchas veces contradictorios.

    • El Proceso de Desarrollo: si nos encontramos desarrollando software de mediano o gran porte, ésto hace que necesitemos trabajar en equipo, en conjunto con uno o más desarrolladores. Si bien ésto suena simple, trabajar en equipo requiere coordinar y definir metodologías de trabajo, nomenclaturas de archivos, estilos, efectos visuales, entre otras cosas; además muchas veces trae aparejada la tarea de desarrollar de forma que tal que una modificación realizada por una persona no vuelva atrás o cambie una funcionalidad hecha por otro desarrollador.

    • La Flexibilidad del software: muy raras veces un prototipo o primera versión de un sistema que desarrollados será lo que finalmente nuestro cliente use, generalmente el mismo querrá cambios o agregar/eliminar funcionalidades; puesto que el software constantemente cambia. En algunos proyectos de desarrollo algunas veces lamentablemente se debe cambiar de lenguaje o de tecnología, porque lo diseñado no puede ser escalable a las necesidades del cliente; ésto es visto y considerado como una gran pérdida de tiempo.

    • La Reutilización del código: al desarrollar sistemas similares o con características iguales, en el mundo de la Ingeniería del Software se trata de aplicar el principio de “No reinventar la rueda”, ésto quiere decir que nuestro código debe poder ser reusable en varios escenarios y en la medida de lo posible en distintos sistemas.

Principalmente por éstos 4 puntos decimos que el software es complejo y que su ciclo de desarrollo también lo es; es por ésto que se hace necesario contar con alguna herramienta o tecnología que nos permita sortear éstos problemas.

Definimos a la POO como a un paradigma de programación, es decir a una forma de analizar, diseñar y realizar soluciones en el mundo del software. Existen distintos paradigmas de programación (funcional, lógico, procedural, entre otros) los cuales tienen como objetivo el dar un marco de trabajo para el diseño y desarrollo de soluciones de software. En el caso de la POO vamos a pensar, diseñar e implementar nuestro sistema como un conjunto de Objetos que se comunican entre sí mediante mensajes para llevar a cabo un objetivo en común.

Objetivos que platea este Paradigma

    • Escribir software fácilmente modificable y escalable.

    • Escribir software reusable.

    • Disponer de un modelo natural para representar un dominio.


Ventajas que ofrece

    • Fomenta la reutilización del código.

    • Permite desarrollar software más flexible al cambio y escalable.

    • Nos ofrece una forma de pensar más cercana a la realidad de las personas.


Para desarrollar aplicaciones orientadas a objetos necesitamos poseer la cualidad de la abstracción. Esto consiste en aislar un elemento de su contexto y tomar de él sólo las características que importan del mismo y las acciones que éste puede realizar. 

Un mismo objeto (una Persona) puede ser observado desde diferentes puntos de vista dependiendo de su contexto y de lo que se desee modelar. Si estamos desarrollando un sistema de gestión de personal para una empresa, importaran sus datos personales (sexo, fecha de nacimiento, DNI); mientras que si programamos una aplicación para resultados de encuestas importarán más otros datos (opinión, postura a favor de ciertos aspectos, etc). 

El objetivo de la Abstracción es identificar objetos en el dominio del problema que buscamos resolver y definir claramente que es lo que cada uno puede hacer (comportamiento) y qué información nos interesa almacenar de cada uno (atributos).

En el mundo de la POO nos encontraremos con dos herramientas, las cuales son esenciales para el funcionamiento del paradigma, las cuales son:


Clases 

    • Son el molde o modelo sobre el cual se crean los objetos.

    • Definen atributos y comportamientos comunes a todos los objetos de la misma clase.

    • Son considerados como un plano o molde sobre el cual creamos los objetos.

    • Son definiciones estáticas.


Objetos

    • Están modelados en función de una clase.

    • Son entidades dinámicas, es decir, son los que se ejecutan durante el ciclo de vida de un programa.

    • Son instancias de una clase dada.


Denominaremos de ésta forma a las entidades que se ejecutan en nuestro sistema para poder llevar a cabo un objetivo o funcionalidad, colaborando entre sí. Todo objeto es capaz de almacenar información en sus atributos y de realizar operaciones a través de sus métodos. 

Cada entidad almacenará información dentro de su estructura, lo que le permitirá brindar o modificar esa información y además podrá interactuar con sí mismo o con otros objetos mediante operaciones o métodos, generalmente son éstos métodos o procedimientos los que permiten modificar u obtener información almacenada en una entidad u objeto. Podemos decir que la información que un objeto posee determina su estado, mientras que los métodos que el mismo contiene determinan su comportamiento. 

Definimos como estado, a el valor que contienen todos los atributos de un objeto en un momento dado. Una persona puede tener 33 años, tener por nombre Juan y por Apellido González y poseer una altura de 1.78, en éste caso estamos haciendo referencia a atributos deseables para un objeto de tipo Persona. 

Es recomendable no permitir modificar la información de un objeto (almacenada en sus atributos) con total libertad, sino que publicar esa información a través de su comportamiento o métodos, ésto se conoce como encapsulamiento.  A la hora de clasificar los atributos o propiedades de un objeto, podemos decir que el mismo tiene propiedades inherentes a cada objeto (propiedades dinámicas) y propiedades comunes a todos los objetos del mismo tipo (propiedades estáticas), exploraremos éstos conceptos más adelante en este capítulo.

Además de almacenar información, todo objeto puede realizar distintas acciones, comunicándose o no con otros objetos. Definimos ésta característica como el comportamiento de un objeto, el cual va a estar determinado por los distintos métodos que el objeto que posea y utilice. Es deseable en la mayoría de los casos que nos comuniquemos con un objeto a través de sus métodos y que, en todo caso, sean ellos los que modifican o publiquen la información del mismo.

Cada objeto es una instancia de una clase dada, por lo tanto debe ser identificable en todo momento y diferenciarse del resto. No debemos confundir el estado de un objeto con su identidad, ya que el estado puede ir variando en la medida en que cambia el valor de los atributos de un objeto, pero su identidad permanece constante durante todo su ciclo de vida. En muchos lenguajes de programación la identidad está definida por una variable que almacena la dirección de memoria de un objeto (similar al concepto de puntero en otros lenguajes), a través de la cual podemos acceder al comportamiento y a los métodos del mismo. 

En una aplicación OOP nos encontraremos con muchas instancias de objeto en la memoria, las cuales no actúan por sí solas, sino que colaboran en conjunto para llevar a cabo la ejecución del programa. Para realizar esta tarea dichos objetos se comunican a través de mensajes, es decir existirán objetos que emiten y objetos que reciben mensajes, por ejemplo un objeto Persona podría crear un objeto Venta y luego podría comunicarse con él mismo y solicitarle que realice una transacción, esta comunicación se da mediante el envío de un mensaje desde el primer objeto al segundo. Generalmente el objeto receptor del mensaje ejecuta un método como respuesta al llamado del mensaje.

Así como definimos a los objetos como entidades dinámicas en la ejecución de una aplicación POO, definiremos a las clases como entidades estáticas que se encargan de la definición de los atributos y métodos comunes a todos los objetos que sean instancias de ellas. Es decir, una clase es como un molde que define todo lo común que luego será utilizado por todos los objetos que sean instancias de sí. 

Un error muy común en el mundo de la POO es confundir y utilizar los términos “Objeto” y “Clase” en forma similar, veamos algunas diferencias:

    • En primer lugar las clases sirven como herramienta para la definición de atributos y métodos, pero solamente realizan esa tarea. Los objetos en cambio son los que llevan a cabo la ejecución del programa, es decir, contienen valores dentro de sus atributos y ejecutan el comportamiento de sus métodos, todo ésto de acuerdo a la clase de la que son instancia.

    • También podemos definir como diferencia a la relación que existe entre los mismos, es decir, un objeto es instancia de una sola clase, pero de una clase pueden existir N instancias (objetos) distintos al mismo tiempo.

Por lo tanto, resumiendo, las clases son entidades de definición (estáticas) y los objetos entidades de ejecución (dinámicas). 

Llamamos entonces instanciación al proceso de creación de un nuevo objeto (instancia) a partir de su clase dada y clasificación al proceso de definir la clase de la cual es instancia un objeto.

Al existir en la memoria en un momento dado, podemos decir que el estado de un objeto está formado por el valor de todos los atributos del mismo en un momento dado. Es decir que, por ejemplo, una instancia de la clase Persona tendrá un nombre, apellido y edad en un momento dado del programa, una instancia de la clase Venta tendrá una fecha de venta y un estado, una instancia de la clase Usuario tendrá un nombre y contraseña. Con esto queremos decir que el valor de los atributos define el estado de un objeto, el cual va variando con la ejecución de la aplicación.

Me quedo super largo, voy a tener que hacer varios post...

sábado, 18 de enero de 2025

El Operador |> de Elixir y sus equivalentes en otros lenguajes


En Elixir, el operador |> pasa el resultado de una expresión como el primer argumento de la siguiente función. Ya lo explicamos en el post anterior. 


" hello "

|> String.trim()

|> String.upcase()

Resultado: "HELLO"


Este diseño promueve una lectura fluida del código, eliminando la necesidad de paréntesis anidados.


F#, un lenguaje funcional inspirado en ML, también tiene un operador pipe |> con un propósito similar al de Elixir.


" hello "

|> String.trim

|> String.uppercase


El operador en F# permite que el flujo de datos sea explícito, facilitando la composición de funciones.


Python no tiene un operador pipe nativo, pero existen bibliotecas que lo emulan, como `pipe` o `toolz`. Sin embargo, sin bibliotecas adicionales, puedes lograr algo similar con reduce:


from functools import reduce


data = " hello "

result = reduce(lambda acc, fn: fn(acc), [str.strip, str.upper], data)

print(result)  # HELLO


Con una biblioteca como pipe:


from pipe import Pipe


result = " hello " | Pipe(str.strip) | Pipe(str.upper)

print(result)  # HELLO


JavaScript aún no tiene un operador pipe oficial, pero hay una propuesta en desarrollo en el comité TC39 (etapa 2 al momento de escribir). Con esta propuesta, el pipe se usa de la siguiente manera:


" hello "

  |> (x => x.trim())

  |> (x => x.toUpperCase());


Por ahora, puedes emularlo con funciones:


const pipeline = (...fns) => x => fns.reduce((v, f) => f(v), x);


const result = pipeline(

  x => x.trim(),

  x => x.toUpperCase()

)(" hello ");

console.log(result); // HELLO


Scala no tiene un operador pipe nativo, pero es posible definir uno:


implicit class PipeOps[T](val value: T) extends AnyVal {

  def |>[R](f: T => R): R = f(value)

}


val result = " hello "

  |> (_.trim)

  |> (_.toUpperCase)

println(result) // HELLO


En C#, aunque no existe un operador pipe, los métodos de extensión de LINQ se comportan de manera similar:


string result = " hello "

    .Trim()

    .ToUpper();

Console.WriteLine(result); // HELLO


El concepto detrás del operador pipe (`|>`) es universal: facilita la composición de funciones y mejora la legibilidad. Aunque su implementación varía entre lenguajes, su propósito sigue siendo el mismo: transformar datos paso a paso de manera clara y concisa.


jueves, 28 de noviembre de 2024

Listas por Comprensión en Python


Las listas por comprensión (o list comprehensions) son una característica poderosa y expresiva de Python que permite construir listas nuevas a partir de iterables existentes, todo ello en una sola línea de código. Son legibles, concisas y, a menudo, más eficientes que los bucles tradicionales.

Son una forma de crear listas en Python utilizando una sintaxis compacta basada en una expresión, un iterador y (opcionalmente) una condición.

Con la forma : [nueva_expresión for elemento in iterable if condición]

  • nueva_expresión: La operación o transformación a aplicar a cada elemento.
  • for elemento in iterable: Itera sobre los elementos de un iterable (como una lista o rango).
  • if condición(opcional): Filtra los elementos según una condición.


Convertir una lista de números en sus cuadrados:


numeros = [1, 2, 3, 4, 5]

cuadrados = [n**2 for n in numeros]

print(cuadrados)

# Salida: [1, 4, 9, 16, 25]


Seleccionar solo los números pares antes de calcular sus cuadrados:


numeros = [1, 2, 3, 4, 5]

cuadrados_pares = [n**2 for n in numeros if n % 2 == 0]

print(cuadrados_pares)

# Salida: [4, 16]


Puedes llamar funciones dentro de la expresión:


nombres = ["Ana", "Bernardo", "Carla", "Diego"]

longitudes = [len(nombre) for nombre in nombres]

print(longitudes)

# Salida: [3, 8, 5, 5]


Crear combinaciones de elementos con múltiples iteradores:


colores = ["rojo", "verde", "azul"]

tamaños = ["pequeño", "mediano", "grande"]


combinaciones = [(color, tamaño) for color in colores for tamaño in tamaños]

print(combinaciones)

# Salida: [('rojo', 'pequeño'), ('rojo', 'mediano'), ..., ('azul', 'grande')]


Usar listas por comprensión con otras estructuras, como diccionarios por comprensión


nombres = ["Ana", "Bernardo", "Carla"]

diccionario = {nombre: len(nombre) for nombre in nombres}

print(diccionario)

# Salida: {'Ana': 3, 'Bernardo': 8, 'Carla': 5}


Conjuntos: 

numeros = [1, 2, 2, 3, 4, 4]

pares = {n for n in numeros if n % 2 == 0}

print(pares)

# Salida: {2, 4}


Para listas grandes, usa generadores para ahorrar memoria:


numeros = (n**2 for n in range(10))

print(list(numeros))

# Salida: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]


Aunque son poderosas, a veces es mejor optar por un bucle tradicional:

- Cuando la lógica es demasiado compleja y afecta la legibilidad.

- Si necesitas manejar excepciones o realizar múltiples pasos intermedios.


Las listas por comprensión son una herramienta esencial para escribir código Python limpio y eficiente. Con práctica, dominarás su uso y aprovecharás al máximo su flexibilidad. ¿Te atreves a crear tus propias transformaciones?


sábado, 9 de noviembre de 2024

GraalVM + sistema operativo = GraalOS


GraalOS es una iniciativa experimental que integra la tecnología de GraalVM directamente en el sistema operativo, permitiendo que las aplicaciones, especialmente las desarrolladas en lenguajes JVM (Java, Scala, Kotlin), se ejecuten de manera más eficiente y directa sobre el hardware. GraalOS busca ser un sistema operativo minimalista y optimizado para ejecutar aplicaciones de alto rendimiento, proporcionando un entorno ideal para microservicios, procesamiento en la nube y aplicaciones en tiempo real.

Las principales características de GraalOS son: 

  1. Soporte Nativo para Lenguajes JVM: GraalOS permite ejecutar código de JVM directamente sobre el sistema operativo sin capas intermedias, ofreciendo un rendimiento nativo para lenguajes como Java, Kotlin y Scala.
  2. Integración con GraalVM: GraalOS está construido sobre la base de GraalVM, lo que permite la compilación AOT (Ahead-of-Time) y el uso de `native-image` para generar binarios nativos que corren eficientemente sobre el hardware.
  3. Ecosistema Multilenguaje: Aunque está optimizado para lenguajes de la JVM, GraalOS también soporta otros lenguajes como JavaScript, Python y R, aprovechando la compatibilidad de GraalVM.
  4. Optimización para Microservicios: GraalOS está diseñado para ejecutarse en contenedores ligeros, ideales para arquitecturas de microservicios y entornos de computación en la nube.

Uno de los puntos fuertes de GraalOS es el uso de la tecnología de compilación Ahead-of-Time (AOT) de GraalVM. La compilación AOT permite que el código de JVM se convierta en código nativo, lo cual mejora significativamente el tiempo de inicio y reduce el uso de memoria.

native-image -jar tu_aplicacion.jar

Este comando convierte un archivo JAR en un binario nativo, optimizado y listo para ejecutarse en GraalOS. Los binarios nativos generados pueden arrancar casi instantáneamente y son ideales para aplicaciones que requieren respuesta en tiempo real.

GraalOS ofrece un entorno perfecto para el despliegue de aplicaciones en la nube gracias a su integración optimizada con GraalVM. Además, permite manejar aplicaciones en tiempo real gracias a su bajo tiempo de respuesta y consumo de recursos. Su diseño minimalista y eficiente hace que sea una opción atractiva para desarrolladores que busquen optimizar costos y rendimiento en entornos de microservicios o serverless.

Aunque GraalOS es experimental, se puede probar en entornos de contenedores o como un sistema operativo en máquinas virtuales para evaluar su rendimiento en aplicaciones específicas. Para comenzar, puedes instalar GraalOS en una máquina virtual y luego utilizar GraalVM para compilar y ejecutar aplicaciones.


apt update && apt install graalos


GraalOS representa un avance en la forma en que interactuamos con el hardware a nivel de sistema operativo para ejecutar aplicaciones nativas. Aunque en sus primeras etapas, su integración con GraalVM abre la puerta a nuevas oportunidades en la ejecución de aplicaciones de alto rendimiento y microservicios en la nube.

Con una promesa de rendimiento optimizado, tiempos de respuesta ultrarrápidos y soporte multilenguaje, GraalOS podría transformar la forma en que desarrollamos e implementamos aplicaciones nativas.

Dejo like : 

https://blogs.oracle.com/java/post/introducing-graalos

https://graal.cloud/

jueves, 31 de octubre de 2024

Le pregunte a la IA : ¿Qué lenguaje tiene las mejores perspectivas de futuro: Python, Java o JavaScript?

Le pregunte a la IA : ¿Qué lenguaje tiene las mejores perspectivas de futuro: Python, Java o JavaScript? y como me gusto mucho su respuesta se las comparto: 

Para evaluar el futuro de Python, Java, y JavaScript, consideremos cómo se están usando, su versatilidad y las tendencias que los impulsan en áreas clave:


Python

   - Ventajas: Python es ampliamente valorado por su legibilidad y simplicidad, lo cual lo hace popular tanto entre principiantes como en sectores avanzados como la ciencia de datos, la inteligencia artificial (IA), y el análisis de datos. Su ecosistema ofrece un gran soporte en estas áreas, con bibliotecas como TensorFlow, Pandas, y NumPy.

   - Perspectivas de Futuro: Su relevancia en IA y ciencia de datos seguirá creciendo, dado que es el lenguaje de referencia en estos campos. Aunque tiene limitaciones de rendimiento comparado con lenguajes de bajo nivel, las mejoras continuas en sus bibliotecas y en plataformas de soporte (como el uso de PyPy y la integración con código en C++) pueden mitigar estas limitaciones. Sin embargo, en áreas que demandan alto rendimiento en tiempo real, como juegos y aplicaciones de alta concurrencia, su uso podría ser limitado.


Java

   - Ventajas: Java es conocido por su estabilidad y robustez, especialmente en entornos empresariales y sistemas a gran escala. La JVM (Java Virtual Machine) permite que las aplicaciones en Java se ejecuten de manera confiable en múltiples plataformas. Java también es ampliamente utilizado en aplicaciones de Android y sigue siendo un estándar en muchas empresas.

   - Perspectivas de Futuro: La madurez y confiabilidad de Java aseguran su presencia en grandes empresas y aplicaciones financieras, que demandan sistemas sólidos y escalables. Recientemente, con mejoras de rendimiento en las versiones más nuevas y características de programación moderna (como pattern matching y records), Java sigue evolucionando y atrayendo a una nueva generación de desarrolladores. Su uso en aplicaciones web modernas y su compatibilidad con entornos de nube son elementos clave que aseguran su vigencia en el mercado.


JavaScript

   - Ventajas: JavaScript es el lenguaje dominante en el desarrollo web, y su versatilidad ha crecido enormemente con el ecosistema Node.js, lo cual permite su uso tanto en el frontend como en el backend. Su capacidad para construir aplicaciones de una sola página (SPA), junto con frameworks como React, Vue, y Angular, le ha dado un rol esencial en la web moderna.

   - Perspectivas de Futuro: La demanda de aplicaciones web interactivas y rápidas respalda el crecimiento de JavaScript. Además, su capacidad de integrarse con **WebAssembly (Wasm)** ha ampliado su uso en aplicaciones de alto rendimiento en el navegador. Node.js sigue siendo relevante para el backend, y con la evolución continua del lenguaje (por ejemplo, ES2022), JavaScript se mantiene competitivo. 


- Python: dominará en ciencia de datos, IA y aplicaciones académicas; su simplicidad asegura su uso entre nuevos programadores.

- Java: mantiene su posición en el desarrollo empresarial y de sistemas complejos; su futuro está asegurado en estos sectores.

- JavaScript: seguirá siendo fundamental en el desarrollo web, y su interoperabilidad con WebAssembly amplía sus horizontes.


En general, la elección del lenguaje con las "mejores perspectivas" depende del campo específico. Para IA y datos, Python; para sistemas empresariales, Java; y para la web y aplicaciones de usuario, JavaScript. Cada uno tiene un rol fuerte y perspectivas sólidas en sus respectivos dominios.


Que piensan? 

miércoles, 23 de octubre de 2024

Tipos Unión en Python


Python introdujo una forma más sencilla de manejar los tipos unión a partir de la versión 3.10, utilizando el operador `|`.

Antes de Python 3.10, las uniones de tipos se representaban usando `Union` del módulo `typing`. Por ejemplo:


from typing import Union


def procesar_valor(valor: Union[int, float]) -> None:

    print(valor)



Esto indica que `valor` puede ser un `int` o un `float`. Sin embargo, esta sintaxis fue simplificada en Python 3.10 con el uso del operador `|`, lo que mejora la legibilidad:


def procesar_valor(valor: int | float) -> None:

    print(valor)


Ambas formas son equivalentes, pero la nueva es más concisa y fácil de leer.

 Ejemplo práctico:


def manejar_respuesta(respuesta: str | None) -> str:

    if respuesta is None:

        return "No hay respuesta"

    return respuesta


TypeScript, un superconjunto de JavaScript, también soporta tipos unión, permitiendo que una variable pueda tener más de un tipo. Al igual que Python, utiliza un símbolo para definir uniones, en este caso también el `|`:


function procesarValor(valor: number | string): void {

    console.log(valor);

}


TypeScript es un lenguaje estáticamente tipado, lo que significa que el compilador verifica los tipos en tiempo de compilación. Si una operación no es válida para uno de los tipos en la unión, el compilador lanzará un error. Python, por otro lado, realiza la verificación de tipos solo cuando se ejecuta (en tiempo de ejecución).

Intersecciones: TypeScript tiene algo llamado tipos intersección (`&`), donde un valor debe cumplir con todas las condiciones de varios tipos al mismo tiempo, lo que no existe en Python.


function combinar(valores: string | number[]): number {

    if (typeof valores === 'string') {

        return valores.length;

    } else {

        return valores.reduce((a, b) => a + b, 0);

    }

}


En este caso, `valores` puede ser una cadena o un arreglo de números, y se maneja cada tipo de forma separada.

El concepto de tipos unión es fundamental para manejar situaciones en las que un valor puede ser de varios tipos. En Python, con la introducción del operador `|` en la versión 3.10, el manejo de uniones se ha vuelto más simple y elegante. 

Los tipos unión son una excelente herramienta en lenguajes con tipado opcional o estático, permitiendo crear código más robusto y manejable cuando se espera que los datos puedan variar en su forma.

miércoles, 2 de octubre de 2024

DIPLOMATURA INTRODUCCIÓN A LA PROGRAMACIÓN EN PYTHON


La Diplomatura de Introducción a la Programación con Python es una propuesta diseñada para dar respuesta a una demanda específica de diferentes entornos públicos y privados, así como para proporcionar a los y las participantes una sólida base en programación utilizando el lenguaje Python. Con un enfoque accesible y versátil, la diplomatura busca democratizar el conocimiento tecnológico, preparando a quienes la realicen para enfrentar los desafíos digitales de la actualidad.

La diplomatura consta de 7 módulos con una duración horaria total de 220 horas reloj. Los y las participantes adquirirán habilidades prácticas en programación, aprenderán a desarrollar aplicaciones web simples, trabajarán con bases de datos y se familiarizarán con herramientas esenciales. El enfoque práctico del trayecto formativo se refuerza mediante proyectos reales, permitiendo a los y las estudiantes aplicar de inmediato sus conocimientos en escenarios del mundo real.


Dejo link: https://fcyt.uader.edu.ar/diplomaturapython/

viernes, 23 de agosto de 2024

lunes, 19 de agosto de 2024

Se encuentran abiertas las inscripciones para los cursos Gugler!!!

¡Tengo grandes noticias! Estoy emocionado de anunciar que ya están abiertas las inscripciones para los tan esperados cursos Gugler. Si estás buscando avanzar en tu carrera, aprender nuevas habilidades, o simplemente profundizar tus conocimientos en áreas tecnológicas, ¡estos cursos son para ti!







Inscripciones abiertas del segundo cuatrimestre 2024. Inscripciones.gugler.com.ar