Cursos Gugler Segundo Cuatrimestre 2025 - https://mi.gugler.com.ar
Translate
domingo, 31 de agosto de 2025
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
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:
- 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.
- 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.
- 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.
- 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 :
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.