Translate
miércoles, 26 de marzo de 2025
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.
lunes, 24 de marzo de 2025
Diferencias entre @Component, @Controller, @Service y @Repository en Spring Boot
Spring Boot proporciona varias anotaciones para marcar clases como beans dentro del contenedor de Spring. Entre ellas, @Component, @Controller, @Service y @Repository son las más utilizadas. Aunque todas registran un bean dentro del contexto de Spring, cada una tiene un propósito específico. A continuación, explicamos sus diferencias y usos recomendados.
@Component es la anotación más genérica y sirve para marcar cualquier clase como un bean de Spring. Cualquier clase anotada con @Component será detectada automáticamente por el escaneo de componentes de Spring y registrada en el contexto de la aplicación.
@Component
public class MiComponente {
public void hacerAlgo() {
System.out.println("Ejecutando lógica en MiComponente");
}
}
Se recomienda usar @Component cuando la clase no encaje específicamente en @Controller, @Service o @Repository.
@Controller es una especialización de @Component que se utiliza en el contexto de Spring MVC para manejar solicitudes HTTP. Las clases anotadas con`@Controller se encargan de procesar peticiones web y devolver respuestas.
@Controller
public class MiControlador {
@GetMapping("/saludo")
public String saludo(Model model) {
model.addAttribute("mensaje", "¡Hola desde el controlador!");
return "saludo";
}
}
Si el controlador necesita devolver solo datos en formato JSON o XML, se recomienda usar @RestController, que combina`@Controller y @ResponseBody.
@Service también es una especialización de @Component, pero se utiliza para indicar que una clase contiene lógica de negocio. Su uso es más semántico, ayudando a la organización y mantenimiento del código.
@Service
public class MiServicio {
public String obtenerSaludo() {
return "Hola desde el servicio";
}
}
Se recomienda usar @Service para clases que encapsulen lógica de negocio y sean reutilizables en distintos lugares de la aplicación.
@Repository es otra especialización de @Component, pero está orientada a la capa de acceso a datos. Además de marcar la clase como un bean, @Repository proporciona integración con mecanismos de persistencia, como la gestión automática de excepciones relacionadas con bases de datos.
@Repository
public interface MiRepositorio extends JpaRepository<Entidad, Long> {
}
Spring usa @Repository para traducir excepciones de bases de datos en excepciones no verificadas de Spring (DataAccessException).
Cada anotación tiene su uso específico y contribuye a mantener una arquitectura limpia y organizada. Elegir la anotación adecuada ayuda a mejorar la mantenibilidad y escalabilidad de la aplicación.
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.
viernes, 21 de marzo de 2025
Número variable de argumentos en C#
En C#, la palabra clave params se usa para permitir que un método acepte un número variable de argumentos del mismo tipo sin necesidad de definir múltiples sobrecargas.
¿Cómo funciona?
- Solo puede haber un parámetro params por método, y debe ser el último parámetro en la lista.
- El argumento pasado puede ser una lista de valores separados por comas o un array del tipo especificado.
Veamos un ejemplo:
using System;
class Program
{
static void Main()
{
ImprimirNumeros(1, 2, 3, 4, 5);
ImprimirNumeros(10, 20);
ImprimirNumeros(); // No pasa nada, la lista puede estar vacía
}
static void ImprimirNumeros(params int[] numeros)
{
foreach (var num in numeros)
{
Console.Write(num + " ");
}
Console.WriteLine();
}
}
La salida va a ser:
1 2 3 4 5
10 20
También puedes pasar un array en lugar de valores separados por comas:
int[] valores = { 100, 200, 300 };
ImprimirNumeros(valores);
Puedes mezclar params con otros parámetros, pero debe estar al final:
static void Saludar(string mensaje, params string[] nombres)
{
foreach (var nombre in nombres)
{
Console.WriteLine($"{mensaje}, {nombre}!");
}
}
// Llamadas válidas:
Saludar("Hola", "Juan", "Ana", "Luis");
Saludar("Bienvenido", "Carlos");
Y la salida va a ser:
Hola, Juan!
Hola, Ana!
Hola, Luis!
Bienvenido, Carlos!
¿Cuándo usar params?
- Cuando quieres un método flexible sin definir múltiples sobrecargas.
- Para métodos como Console.WriteLine(), que pueden recibir cualquier cantidad de argumentos.
- Para situaciones en las que puede haber 0 o más parámetros opcionales sin usar List<T> o IEnumerable<T>.
Alquimista de código
Les quiero recomendar el blog alquimista de código; es un blog de muy buena calidad y con mucha información y opiniones. Esta bueno!
También cuenta con mucha información de diferentes lenguajes de programación no solo los más conocidos sino tambien nuevos y no tan nuevos. Si les gusta mi blog, seguro este les va a gustar.
Dejo link:
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, 19 de marzo de 2025
Funciones Lambda en C++
Las funciones lambda en C++ permiten definir funciones anónimas de manera concisa y flexible. Introducidas en C++11 y mejoradas en versiones posteriores, las lambdas son muy útiles para expresiones cortas y para capturar variables del contexto circundante.
La sintaxis general de una lambda es:
[captura](parámetros) -> tipo_de_retorno { cuerpo };
Veamos un ejemplo:
#include <iostream>
int main() {
auto suma = [](int a, int b) -> int {
return a + b;
};
std::cout << "La suma es: " << suma(3, 4) << std::endl;
return 0;
}
Las lambdas pueden capturar variables del contexto donde se definen. Su puede captura por Valor
#include <iostream>
int main() {
int x = 10;
auto lambda = [x]() { std::cout << "x = " << x << std::endl; };
lambda();
return 0;
}
Como x se captura por valor, cualquier cambio dentro de la lambda no afectará la variable original.
Y captura por Referencia:
#include <iostream>
int main() {
int x = 10;
auto lambda = [&x]() { x += 5; };
lambda();
std::cout << "x después de la lambda: " << x << std::endl;
return 0;
}
Dado que x se captura por referencia, su modificación dentro de la lambda afecta la variable original.
Se puede capturar todo el entorno automáticamente:
- [=] captura todas las variables por valor.
- [&] captura todas las variables por referencia.
#include <iostream>
int main() {
int a = 5, b = 10;
auto lambda = [=]() { std::cout << "a + b = " << (a + b) << std::endl; };
lambda();
return 0;
}
Las lambdas pueden almacenarse en std::function para mayor flexibilidad:
#include <iostream>
#include <functional>
int main() {
std::function<int(int, int)> multiplicar = [](int x, int y) { return x * y; };
std::cout << "Multiplicación: " << multiplicar(3, 4) << std::endl;
return 0;
}
Las lambdas son útiles para definir comparaciones personalizadas:
Por ejemplo:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> nums = {3, 1, 4, 1, 5, 9};
std::sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b;
});
for (int n : nums) std::cout << n << " ";
return 0;
}
Las funciones lambda en C++ permiten definir funciones anónimas de manera flexible y eficiente. Son especialmente útiles en programación funcional, expresiones cortas y manejo de algoritmos en la STL.
sábado, 15 de marzo de 2025
Clases Selladas y coincidencia de patrones en Java
Con la evolución de Java, dos características han tomado mayor protagonismo: las clases selladas (sealed classes) y el coincidencia de patrones (pattern matching). La combinación de ambas permite escribir código más seguro, expresivo y mantenible.
Las clases selladas permiten restringir qué clases pueden extender o implementar una clase o interfaz. Se declaran con la palabra clave sealed y deben especificar sus subclases con permits.
Por ejemplo:
public sealed class Figura permits Circulo, Poligono {}
public final class Circulo extends Figura {}
public sealed class Poligono extends Figura permits Triangulo {}
public non-sealed class Triangulo extends Poligono {}
Aquí, Figura solo puede ser extendida por Circulo y Poligono, mientras que Triangulo puede ser extendido libremente.
El pattern matching simplifica la lógica condicional al combinar la comprobación de tipos y la conversión en una sola operación.
Por ejemplo:
Object obj = "Hola, mundo!";
if (obj instanceof String s) {
System.out.println(s.toUpperCase());
}
Si obj es un String, se asigna automáticamente a s, evitando el casting manual.
Tambien lo podemos usar con switch:
static void procesar(Object obj) {
switch (obj) {
case String s -> System.out.println("Es una cadena: " + s.toUpperCase());
case Integer i -> System.out.println("Es un número: " + (i * 2));
default -> System.out.println("Tipo no soportado");
}
}
El switch maneja diferentes tipos de objetos de manera más clara y concisa.
La combinación de ambas características permite que el compilador conozca todas las posibles subclases en tiempo de compilación, facilitando el uso de switch y garantizando que se manejen todos los casos.
Por ejemplo:
public sealed interface Operacion permits Suma, Resta, Multiplicacion {}
public record Suma(int a, int b) implements Operacion {}
public record Resta(int a, int b) implements Operacion {}
public record Multiplicacion(int a, int b) implements Operacion {}
public int evaluar(Operacion op) {
return switch (op) {
case Suma s -> s.a() + s.b();
case Resta r -> r.a() - r.b();
case Multiplicacion m -> m.a() * m.b();
};
}
Aquí, el switch cubre todas las implementaciones posibles de Operacion, asegurando que no haya casos no manejados.
Las clases selladas y el pattern matching en Java permiten escribir código más seguro y expresivo. Al proporcionar control sobre la herencia y simplificar la lógica condicional, facilitan la creación de software más mantenible y alineado con las tendencias modernas del desarrollo en Java.
viernes, 14 de marzo de 2025
Vine: Un Lenguaje Minimalista y Funcional
Vine es un nuevo lenguaje de programación experimental basado en redes de interacción.
Vine es un lenguaje multiparadigma que ofrece una interoperabilidad fluida entre patrones funcionales e imperativos.
Vamos por parte ¿Qué son las redes de interacción? : Las redes de interacción son una representación gráfica de los cálculos en un programa, lo que permite una evaluación más eficiente y altamente paralela. En lugar de ejecutarse como instrucciones secuenciales, las operaciones en Vine pueden reorganizarse dinámicamente para optimizar la ejecución.
Vine está diseñado para facilitar la programación funcional y la concurrencia. Esto significa que los desarrolladores pueden escribir código más limpio, modular y escalable, sin preocuparse tanto por problemas típicos de la concurrencia, como las condiciones de carrera.
Los beneficios clave de Vine son:
- Ejecución optimizada: Aprovecha mejor los recursos del hardware al paralelizar automáticamente el procesamiento.
- Código más claro: Al estar basado en principios funcionales, evita efectos secundarios y hace que el código sea más fácil de entender y mantener.
- Escalabilidad: Diseñado para manejar aplicaciones concurrentes sin los bloqueos y problemas de sincronización típicos en otros lenguajes.
El lenguaje todavía está en fase temprana de desarrollo, pero sus creadores creen que podría ser una alternativa prometedora para la programación funcional y concurrente en el futuro.
Veamos un ejemplo:
pub fn main(&io: &IO) {
while io.prompt("> ") is Some(line) {
let num = N32::parse(line);
io.println(match num {
Some(num) { fib(num).to_string() }
None { "invalid number" }
});
}
}
pub fn fib(n: N32) -> N32 {
let a = 0;
let b = 1;
while n != 0 {
n -= 1;
(a, b) = (b, a + b);
}
a
}
Muchas de las características de Vine están influenciadas por Rust, y tiene una sintaxis orientada a expresiones, un sistema de tipos y un sistema de módulos similares.
Vine promete ser una herramienta valiosa para aquellos interesados en paradigmas funcionales y en la optimización de aplicaciones concurrentes.
Dejo links:
https://www.infoq.com/news/2025/03/new-programming-language-vine/
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”
martes, 11 de marzo de 2025
Clases sin Nombre y Métodos Principales sin Nombre en Java 21
Con Java 21, se introducen dos características que simplifican el código para desarrolladores: clases sin nombre y métodos main sin nombre. Estas mejoras buscan reducir la cantidad de código boilerplate en programas simples y facilitar el aprendizaje de Java para nuevos desarrolladores.
Las clases sin nombre permiten escribir código sin necesidad de definir explícitamente una clase contenedora. Esto facilita la escritura de programas pequeños o scripts sin la sobrecarga de definir clases innecesarias.
Antes de Java 21, un programa en Java requería una clase con un main:
public class MiClase {
public static void main(String[] args) {
System.out.println("Hola, Java 21!");
}
}
Con las clases sin nombre, ahora puedes escribir simplemente:
System.out.println("Hola, Java 21!");
En Java 21, no es obligatorio definir explícitamente public static void main(String[] args). Ahora el código dentro del archivo fuente se ejecuta directamente.
System.out.println("Ejecutando sin un método 'main' explícito!");
¿Cómo funciona?
- La JVM reconoce el archivo como ejecutable sin necesidad de una clase o main.
- Útil para scripts rápidos o pruebas sencillas.
- Sigue siendo posible definir clases si se necesita una estructura más compleja.
Las clases sin nombre y los métodos main sin nombre en Java 21 son pasos importantes para hacer el lenguaje más accesible y fácil de usar en escenarios rápidos y educativos. Si bien Java sigue siendo un lenguaje de propósito general con una fuerte orientación hacia aplicaciones empresariales, estas mejoras lo acercan más al paradigma de scripting y programación ligera, pero a mi entender nos alejan de los conceptos puros de programación orientada a objetos.
lunes, 10 de marzo de 2025
¿Para qué se usa volatile en C?
En C, volatile es un calificador de tipo que se utiliza para indicar al compilador que el valor de una variable puede cambiar de maneras que el compilador no puede predecir. Esto es crucial en situaciones donde:
- Hardware interactúa con la memoria: En sistemas embebidos, los registros de hardware se mapean a direcciones de memoria. Estos registros pueden cambiar sus valores de forma asíncrona, sin que el código C lo inicie directamente.
- Manejadores de interrupciones: Los manejadores de interrupciones pueden modificar variables que también se utilizan en el código principal. El compilador no puede saber cuándo ocurrirá una interrupción, por lo que necesita saber que estos valores pueden cambiar de forma inesperada.
- Variables compartidas entre hilos (en entornos con hilos): Aunque C estándar no define hilos, en sistemas operativos que los soportan, volatile puede ayudar a prevenir ciertas optimizaciones que serían incorrectas en presencia de acceso concurrente a la memoria.
Los compiladores de C son muy buenos para optimizar el código. Sin embargo, estas optimizaciones pueden ser problemáticas cuando el valor de una variable puede cambiar externamente. Sin volatile, el compilador podría:
Almacenar el valor de una variable en un registro y reutilizar ese valor, en lugar de leerlo de la memoria cada vez.
Eliminar lecturas o escrituras de la variable que considera redundantes.
`volatile` le dice al compilador que evite estas optimizaciones, asegurando que:
- Cada lectura de la variable se realice realmente desde la memoria.
- Cada escritura de la variable se realice realmente en la memoria.
Veamos un ejemplo:
volatile unsigned int * puerto_serial = (unsigned int *)0xFFFF0000; // Registro de hardware
int main() {
unsigned int dato;
dato = *puerto_serial; // Leer el valor del puerto serial
// ... procesar el dato ...
*puerto_serial = 0x0A; // Escribir un valor en el puerto serial
return 0;
}
En este ejemplo, `puerto_serial` es un puntero a un registro de hardware. El calificador volatile asegura que cada lectura y escritura a esta dirección de memoria se realice realmente, evitando que el compilador optimice estas operaciones.
Por lo tanto podemos concluir que:
- volatile no proporciona sincronización de hilos. En entornos multihilo, se necesitan mecanismos de sincronización adicionales (como mutex) para proteger los datos compartidos.
- volatile solo previene la optimización del compilador con respecto a esa variable. No proporciona atomicidad.
- El uso excesivo de `volatile` puede reducir el rendimiento, por lo que solo debe usarse cuando sea necesario.
En resumen, volatile es esencial en C cuando se trabaja con hardware, interrupciones o en situaciones donde el valor de una variable puede cambiar de forma impredecible.
Concurrencia en Erlang parte 15
Para empezar, deberíamos establecer una estructura de directorio estándar de Erlang, que se parece a esto:
ebin/
include/
priv/
src/
El directorio ebin/ es donde irán los archivos una vez que se compilen. El directorio include/ se utiliza para almacenar archivos .hrl que se incluirán en otras aplicaciones; los archivos .hrl privados generalmente se guardan dentro del directorio src/. El directorio priv/ se utiliza para ejecutables que podrían tener que interactuar con Erlang, como controladores específicos y demás. En realidad, no utilizaremos ese directorio para este proyecto. Luego, el último es el directorio src/, donde se almacenan todos los archivos .erl.
En los proyectos estándar de Erlang, esta estructura de directorio puede variar un poco. Se puede agregar un directorio conf/ para archivos de configuración específicos, doc/ para la documentación y lib/ para las bibliotecas de terceros necesarias para que se ejecute su aplicación. Los diferentes productos Erlang del mercado suelen utilizar nombres diferentes a estos, pero los cuatro mencionados anteriormente suelen permanecer iguales dado que son parte de las prácticas OTP estándar.
Spring Native: La Evolución de las Aplicaciones Spring en un Mundo de Desempeño Nativo
Spring Native es un proyecto dentro del ecosistema Spring que permite compilar aplicaciones Spring Boot directamente a imágenes nativas utilizando GraalVM. Esto significa que, en lugar de ejecutar la aplicación en la JVM (Java Virtual Machine), el código se compila en un binario nativo específico de la plataforma.
¿Por qué es importante Spring Native? Como sabran, spring es muy bueno pero pero con la inyección de dependencia y el uso de proxies con lleva a un mayor uso de memoria y a un inicio de aplicación lento. Además que con docker tampoco es una ventaja ser multiplataforma. Con que funcione en docker ya esta. Todos estos problemas son resueltos con Graalvm y spring native.
Entonces podemos nombras los siguientes beneficios:
- Rendimiento Mejorado: Las aplicaciones nativas tienen tiempos de inicio mucho más rápidos y un consumo de memoria significativamente menor en comparación con las aplicaciones que corren sobre la JVM. Esto es crucial para aplicaciones en contenedores (como Docker) y para arquitecturas de microservicios donde la eficiencia es clave.
- Despliegue más Eficiente: El código nativo puede simplificar la infraestructura de despliegue, ya que no depende de tener una JVM instalada. Esto puede ser muy útil en entornos donde el espacio y los recursos son limitados.
- Menor Consumo de Memoria: Las aplicaciones nativas pueden ejecutarse con un consumo de memoria más bajo, lo que hace que sean ideales para entornos de alta escala, como la nube y la ejecución en dispositivos con recursos limitados.
- Reducción de la Complejidad Operacional: Al generar una imagen nativa, el proceso de despliegue es más directo y menos dependiente de las complejidades de la configuración de la JVM. Esto puede mejorar la mantenibilidad y reducir los errores de configuración.
- Mejor Compatibilidad con el Ecosistema de Contenedores: Las imágenes nativas de Spring Boot se integran de forma más eficiente con contenedores como Docker, al permitirles ser más livianas y rápidas al arrancar, lo cual es vital en escenarios donde el escalado rápido y la flexibilidad son necesarios.
Cuando usar Spring Native?:
- Microservicios: Dado que las aplicaciones nativas tienen tiempos de inicio rápidos y un menor uso de memoria, son perfectas para entornos de microservicios donde el arranque rápido y el escalado eficiente son esenciales.
- Sistemas Serverless: Al ser aplicaciones de bajo consumo, pueden integrarse bien con arquitecturas serverless, donde el costo de recursos y el tiempo de ejecución son factores clave.
- IoT y Dispositivos con Recursos Limitados: En aplicaciones que se ejecutan en dispositivos con recursos limitados, como IoT, la eficiencia de Spring Native puede ser un gran beneficio.
Desafíos y Limitaciones:
- Compatibilidad: No todas las bibliotecas de Spring Boot son totalmente compatibles con GraalVM, y algunas características pueden requerir ajustes adicionales.
- Tiempo de compilación: La creación de la imagen nativa puede ser lenta, lo que podría no ser ideal para todos los entornos de desarrollo.
¿Cómo Integrar Spring Native en tu Proyecto?
- Requisitos: Tener un proyecto Spring Boot básico.
- Configuración de GraalVM: Instalar GraalVM y configurar el entorno de desarrollo para soportar la compilación nativa.
- Incluir dependencias nativas: Agregar la dependencia de spring-native en el pom.xml o `build.gradle`.
- Compilación: Usar `mvn spring-boot:build-image o ./gradlew buildNative para generar la imagen nativa.
- Despliegue y pruebas: Probar la imagen nativa generada en el entorno de producción.
Spring Native permite a las aplicaciones Spring aprovechar los beneficios del código nativo: tiempos de arranque rápidos, menor uso de memoria y mayor eficiencia en la ejecución. A medida que las arquitecturas modernas se mueven hacia entornos de microservicios y despliegues en la nube, Spring Native se posiciona como una herramienta clave para la optimización de aplicaciones Spring.
viernes, 7 de marzo de 2025
API de Vectores en Java: Alto Rendimiento con Operaciones SIMD
Java ha introducido mejoras significativas en el rendimiento con la API de Vectores, una funcionalidad experimental desde Java 16 y evolucionada en Java 19 y 20. Esta API permite aprovechar las capacidades de SIMD (Single Instruction, Multiple Data), lo que mejora la eficiencia en cálculos intensivos, como gráficos, procesamiento de señales y machine learning.
La API de Vectores en Java permite realizar operaciones con múltiples valores en paralelo utilizando instrucciones SIMD, optimizando así el uso del hardware subyacente. Está diseñada para ejecutarse de manera más eficiente en CPU modernas con conjuntos de instrucciones avanzados como AVX y NEON.
Las características principales son:
- Operaciones eficientes con vectores: Realiza cálculos numéricos sobre múltiples datos simultáneamente.
- Compatibilidad con JVM y JIT: Se integra con el compilador Just-In-Time para optimizar el rendimiento.
- Optimización automática: Se adapta a la arquitectura del procesador para aprovechar el mejor conjunto de instrucciones disponible.
- API expresiva y segura: Usa un enfoque declarativo y sin riesgo de desbordamiento de memoria.
Veamos un ejemplo:
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
public class VectorExample {
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
public static void main(String[] args) {
float[] a = {1.0f, 2.0f, 3.0f, 4.0f};
float[] b = {5.0f, 6.0f, 7.0f, 8.0f};
float[] result = new float[a.length];
var va = FloatVector.fromArray(SPECIES, a, 0);
var vb = FloatVector.fromArray(SPECIES, b, 0);
var vc = va.add(vb);
vc.intoArray(result, 0);
System.out.println("Resultado: ");
for (float v : result) {
System.out.print(v + " ");
}
}
}
- Se define una especie de vector (SPECIES_PREFERRED) que se ajusta a la arquitectura del CPU.
- Se cargan los datos en vectores FloatVector.
- Se realiza una operación de suma en paralelo.
- Se almacenan los resultados en un array de salida.
En Java 19 y 20 se continuo trabajando obteniendo estas mejoras:
- Mayor estabilidad y optimización en JVM: Se han reducido las sobrecargas en JIT.
- Soporte extendido para más arquitecturas: Ahora es compatible con ARM y RISC-V.
- Nuevas operaciones matemáticas: Más funciones avanzadas como cálculos trigonométricos y logarítmicos.
La API de Vectores en Java es una gran incorporación para quienes necesitan alto rendimiento en operaciones matemáticas y científicas. Con el soporte mejorado en arquitecturas modernas y una mayor optimización en la JVM, esta API se perfila como una herramienta clave para el desarrollo de aplicaciones de alto rendimiento.
miércoles, 5 de marzo de 2025
Cómo Spring Implementa AOP
Spring AOP se basa en el Patrón Proxy y la generación dinámica de clases para aplicar aspectos sin modificar el código fuente original.
Spring AOP crea proxies para interceptar llamadas a métodos y aplicar lógica adicional. Dependiendo de la estructura de la clase objetivo, Spring elige entre dos enfoques:
JDK Dynamic Proxies: Si la clase implementa una interfaz, Spring usa java.lang.reflect.Proxy para generar un proxy en tiempo de ejecución.
CGLIB (Code Generation Library): Si la clase no implementa interfaces, se genera una subclase dinámica con CGLIB.
Veamos un ejemplo de un proxy con JDK:
import java.lang.reflect.*;
interface Servicio {
void ejecutar();
}
class ServicioImpl implements Servicio {
public void ejecutar() {
System.out.println("Ejecutando servicio...");
}
}
class ProxyHandler implements InvocationHandler {
private final Object target;
public ProxyHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("[AOP] Antes de ejecutar");
Object result = method.invoke(target, args);
System.out.println("[AOP] Después de ejecutar");
return result;
}
}
public class Main {
public static void main(String[] args) {
Servicio servicio = (Servicio) Proxy.newProxyInstance(
Servicio.class.getClassLoader(),
new Class[]{Servicio.class},
new ProxyHandler(new ServicioImpl()));
servicio.ejecutar();
}
}
Si la clase no implementa interfaces, Spring usa CGLIB para generar una subclase dinámica.
import net.sf.cglib.proxy.*;
class Servicio {
public void ejecutar() {
System.out.println("Ejecutando servicio...");
}
}
class Interceptor implements MethodInterceptor {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("[AOP] Antes de ejecutar");
Object result = proxy.invokeSuper(obj, args);
System.out.println("[AOP] Después de ejecutar");
return result;
}
}
public class Main {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Servicio.class);
enhancer.setCallback(new Interceptor());
Servicio proxy = (Servicio) enhancer.create();
proxy.ejecutar();
}
}
Spring AOP aplica aspectos mediante proxies dinámicos. Si la clase tiene una interfaz, usa JDK Dynamic Proxies; si no, usa CGLIB para generar una subclase dinámica. Esto permite agregar lógica sin modificar el código fuente ni el bytecode.
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.