Cómo funcionan las @ en Python

¿Qué son las @ en tu código?

¿Alguna vez has visto un @ justo antes de una función en Python y te has preguntado que hace eso ahí? ¡Bienvenido al mundo de los decoradores! Son como chefs que añaden el toque secreto a tus funciones, dándoles un sabor extra sin cambiar la receta original.

Ejemplo básico

def mayusculas(func):
    def wrapper():
        result = func()
        return result.upper()
    return wrapper

@mayusculas
def saludar():
    return 'hola mundo'


print(saludar())
# HOLA MUNDO

Nuestra función saludar() ahora devuelve el saludo en mayúsculas, gracias al decorador @mayusculas. El decorador coge saludar(), la envuelve con wrapper() y nos devuelve una nueva versión de la función, todo sin tocar el código original.

Un ejemplo más interesante

Vamos a hacer algo más práctico. Digamos que quieres saber cuánto tarda una función en ejecutarse. ¡Decorador al canto!

import time

def medir_tiempo(func):
    def wrapper(*args, **kwargs):
        inicio = time.time()
        resultado = func(*args, **kwargs)
        fin = time.time()
        print(f'La función {func.__name__} tardó {fin - inicio:.4f} segundos en ejecutarse.')
        return resultado
    return wrapper

@medir_tiempo
def procesar_datos():
    time.sleep(2)  # Simulamos una tarea que tarda 2 segundos
    return "Datos procesados"


print(procesar_datos())
# La función procesar_datos tardó 2.0001 segundos en ejecutarse.
# Datos procesados

Ahora, cada vez que llamamos a procesar_datos(), nuestro decorador mide el tiempo que tarda en completarse. Sin modificar el código de la función original, logramos algo súper útil.

Decoradores con Argumentos

¿Y si queremos que el decorador acepte parámetros? No hay problema. Imagina que queremos un decorador que repita la ejecución de una función varias veces.

def repetir(n):
    def decorador(func):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                resultado = func(*args, **kwargs)
            return resultado
        return wrapper
    return decorador

@repetir(3)
def decir_hola():
    print('¡Hola!')

decir_hola()
# ¡Hola!
# ¡Hola!
# ¡Hola!

El decorador @repetir(3) hace que la función decir_hola() se ejecute 3 veces. Así de sencillo es agregar comportamiento dinámico a nuestras funciones con un simple decorador.

Conclusión

Los decoradores son una forma muy buena de extender el comportamiento de tus funciones sin tocarlas directamente. Son como agregarle superpoderes a tus funciones, pero sin alterar su ADN. Así que la próxima vez que veas un @ en Python, sabrás que algo de magia está ocurriendo ahí.