Tema avanzados

Decoradores (Decorators)

Un decorador es una función que recibe otra función como argumento, le añade alguna funcionalidad extra y la devuelve modificada, todo sin alterar su código original. Es el pilar fundamental para frameworks como FastAPI o Flask.

def mi_decorador(funcion):
    def envoltura():
        print('1. Acceso antes de ejecutar la función.')
        funcion()
        print('2. Tarea posterior completada.')
    return envoltura

@mi_decorador
def saludar():
    print('   -> Hola Mundo <-')

saludar()    

Programación Asíncrona (Asyncio)

Permite ejecutar múltiples tareas de forma concurrente en un solo hilo. Mientras el programa espera una respuesta lenta (como una consulta a una base de datos o una descarga de internet), aprovecha ese tiempo muerto para ejecutar otra tarea.

import asyncio

async def descargar_datos(id_tarea):
   print(f'Iniciando descarga {id_tarea}...')
   await asyncio.sleep(2) # Simula una espera de red de 2 segundos
   print(f'Descarga {id_tarea} finalizada.')
   return f'Datos {id_tarea}'

async def main():
   # Ejecuta ambas descargas al mismo tiempo de forma concurrente
   resultados = await asyncio.gather(descargar_datos(1), descargar_datos(2))
   print(resultados)

asyncio.run(main())

Generadores y Expresiones Generadoras

A diferencia de las listas que guardan todos sus elementos en la memoria RAM de golpe, los generadores van calculando y entregando los datos uno por uno, bajo demanda. Son indispensables para procesar archivos gigantescos de gigabytes de información sin saturar el sistema.

# Usa 'yield' en lugar de 'return' para crear un generador
def cuenta_regresiva(numero):
    while numero > 0:
        yield numero
        numero -= 1

# No consume memoria hasta que se itera
for valor in cuenta_regresiva(1000000):
    print(valor)
    break                        # Detiene la ejecución consumiendo casi cero RAM

Administradores de Contexto Personalizados (with)

Seguramente has usado with open() para leer archivos. Puedes crear tus propios administradores de contexto usando los métodos mágicos __enter__ y __exit__ para asegurar que recursos costosos (como conexiones a bases de datos o sockets) se cierren siempre correctamente, incluso si ocurre un error catastrófico.

class GestorConexion:
    def __enter__(self):
        print('Conectando a la Base de Datos...')
        return 'CONEXION_ACTIVA'

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('Cerrando la conexión de forma segura.')

with GestorConexion() as db:
    print(f'Ejecutando consultas con: {db}')

Metaprogramación (Metaclasses)

En Python, las clases también son objetos. Una metaclase es la "clase de una clase"; es decir, define cómo se construyen las propias clases. Te permite modificar la estructura de tus objetos en tiempo de diseño, validar que tus programadores cumplan con ciertas reglas o inyectar código automáticamente.

class ControlCalidadMeta(type):
   def __new__(cls,name,bases,dct):
      # Obliga a que todas las clases hijas tengan un método obligatorio
      if 'guardar' not in dct:
         raise TypeError(f"Error: La clase '{name}' debe implementar el método 'guardar'")
      return super().__new__(cls,name,bases,dct)

# Esto lanzará un TypeError inmediatamente al compilar porque le falta 'guardar'
class ModeloUsuario(metaclass=ControlCalidadMeta):
   def inicializar(self):
      pass

Descriptores (Descriptors)

Son la magia oculta detrás de decoradores como @property. Un descriptor te permite personalizar completamente qué pasa cuando se lee (__get__), escribe (__set__) o borra (__delete__) un atributo de un objeto, permitiendo validaciones robustas y limpias.

Descriptors
DescriptorDescripcion
__get__(self, instance, owner)Se activa al leer el atributo.
__set__(self, instance, value)Se activa al escribir/modificar el atributo.
__delete__(self, instance)Se activa al borrar el atributo (del).
class NumeroPositivo:
    """Descriptor que asegura que un atributo sea numérico y mayor que cero."""
    def __init__(self, nombre_atributo):
        self.nombre_atributo = nombre_atributo

    def __get__(self, instancia, clase_propietaria):
        # Si se accede desde la clase (no desde un objeto), devolvemos el descriptor mismo
        if instancia is None:
            return self
        # Retorna el valor guardado en el diccionario interno del objeto
        return instancia.__dict__.get(self.nombre_atributo)

    def __set__(self, instancia, valor):
        # Validaciones estrictas
        if not isinstance(valor, (int, float)):
            raise TypeError(f"El campo '{self.nombre_atributo}' debe ser un número.")
        if valor < 0:
            raise ValueError(f"El campo '{self.nombre_atributo}' no puede ser negativo.")
        
        # Si pasa las pruebas, se guarda en el objeto
        print(f"[Descriptor] Asignando {valor} a '{self.nombre_atributo}'")
        instancia.__dict__[self.nombre_atributo] = valor


# --- Aplicación del Descriptor en nuestra clase de negocio ---
class Articulo:
    # Vinculamos los atributos al comportamiento del descriptor
    precio = NumeroPositivo('precio')
    stock = NumeroPositivo('stock')

    def __init__(self,nombre,precio,stock):
        self.nombre = nombre
        self.precio = precio  # Esto activa automáticamente __set__
        self.stock = stock    # Esto activa automáticamente __set__