Decoradores en Python | Capítulo 20

Los decoradores en Python son una característica poderosa y elegante que permite modificar el comportamiento de funciones o métodos sin alterar su código original. Imagina que tienes una función básica, como una receta de cocina, y quieres “envolverla” con pasos adicionales, como precalentar el horno o limpiar después, sin cambiar la receta en sí. Los decoradores actúan como estos envoltorios reutilizables, haciendo que tu código sea más modular, legible y eficiente. En este capítulo, nos enfocamos en decoradores simples aplicados a funciones, explorando su mecánica paso a paso para que los domines por completo.

Entendiendo las bases: funciones como objetos de primera clase

Antes de sumergirnos en los decoradores, es crucial comprender que en Python, las funciones son objetos de primera clase. Esto significa que puedes tratarlas como cualquier otro objeto: asignarlas a variables, pasarlas como argumentos a otras funciones o incluso retornarlas desde funciones. Esta propiedad es el pilar de los decoradores.

Piensa en una función como un trabajador en una fábrica. Puedes reasignar ese trabajador a otra tarea (asignarlo a una variable), enviarlo a otra fábrica (pasarlo como argumento) o crearlo en el momento y enviarlo de vuelta (retornarlo). Veamos un ejemplo simple para ilustrarlo.

# Definimos una función simple
def saludo(nombre):
    return f"Hola, {nombre}!"

# Asignamos la función a una variable
mi_saludo = saludo

# Llamamos a la función a través de la variable
print(mi_saludo("Mundo"))  # Salida: Hola, Mundo!
Python

Aquí, saludo no es solo un bloque de código; es un objeto que puedes manipular. Este concepto es fundamental porque los decoradores “envuelven” funciones existentes con otras funciones.

Funciones anidadas: el primer paso hacia los decoradores

Ahora, avancemos a las funciones anidadas, que son funciones definidas dentro de otras funciones. Estas permiten crear lógica encapsulada y son esenciales para construir decoradores simples.

Imagina una caja dentro de otra caja: la función interna solo es accesible desde la externa, manteniendo el código organizado y evitando contaminar el espacio global.

# Función externa con una función interna anidada
def externa():
    def interna():
        return "¡Soy la función interna!"
    # Llamamos a la interna solo desde aquí
    return interna()

print(externa())  # Salida: ¡Soy la función interna!
Python

Ejecuta este código guardando el archivo como ejemplo_anidada.py y corriendo python ejemplo_anidada.py en tu terminal. Observa cómo la función interna no existe fuera de externa—intenta llamarla directamente y verás un error. Esta encapsulación es clave para los decoradores, ya que nos permite definir lógica adicional sin exponerla innecesariamente.

Retornando funciones: construyendo el envoltorio

El siguiente paso es retornar funciones desde otras funciones. Esto nos permite crear “fábricas” de funciones personalizadas, que es exactamente lo que hace un decorador en su forma básica.

Analogía cotidiana: imagina una máquina expendedora que, en lugar de darte una bebida, te da una receta personalizada para prepararla en casa. La máquina (función externa) retorna la receta (función interna) lista para usar.

# Función que retorna otra función
def fabrica_saludo(saludo_inicial):
    def saludo_personalizado(nombre):
        return f"{saludo_inicial}, {nombre}!"
    return saludo_personalizado  # Retornamos la función, no la llamamos

# Creamos una función personalizada
hola = fabrica_saludo("Hola")
print(hola("amigo"))  # Salida: Hola, amigo!

# Otra variante
adios = fabrica_saludo("Adiós")
print(adios("amigo"))  # Salida: Adiós, amigo!
Python

Aquí, fabrica_saludo no ejecuta el saludo; solo lo prepara y lo retorna. Puedes ejecutar este ejemplo en un archivo ejemplo_retorno.py con python ejemplo_retorno.py. Nota cómo cada llamada a fabrica_saludo genera una nueva función adaptada—esto es reutilizable y flexible, preparando el terreno para decoradores.

Creando un decorador simple: el envoltorio en acción

Ahora, unimos todo para crear un decorador simple. Un decorador es una función que toma otra función como argumento, la “envuelve” con funcionalidad adicional y retorna una nueva función que incluye ese envoltorio.

Piensa en un regalo: el decorador es el papel de envoltura que añade un toque extra (como registrar el tiempo de ejecución) sin cambiar el regalo original (la función).

# Definimos un decorador simple que imprime un mensaje antes y después
def mi_decorador(funcion_original):
    def envoltorio():
        print("Antes de ejecutar la función original")
        funcion_original()  # Llamamos a la original
        print("Después de ejecutar la función original")
    return envoltorio  # Retornamos la función envuelta

# Aplicamos el decorador manualmente (sin @ por ahora)
def funcion_a_decorar():
    print("¡Función original ejecutándose!")

decorada = mi_decorador(funcion_a_decorar)
decorada()  # Salida: Antes..., ¡Función original...!, Después...
Python

Este es un decorador básico: mi_decorador recibe funcion_a_decorar, crea un envoltorio que añade impresiones, y lo retorna. Ejecuta en ejemplo_decorador.py con python ejemplo_decorador.py. Observa que la función original permanece intacta—solo se envuelve al llamarla.

La sintaxis @ para decoradores: elegancia en una línea

Python ofrece la sintaxis @ para aplicar decoradores de forma limpia y legible, equivalente a la asignación manual que vimos antes. Es como un atajo que hace tu código más profesional.

# El mismo decorador
def mi_decorador(funcion_original):
    def envoltorio():
        print("Antes de ejecutar la función original")
        funcion_original()
        print("Después de ejecutar la función original")
    return envoltorio

# Aplicamos con @
@mi_decorador
def funcion_a_decorar():
    print("¡Función original ejecutándose!")

# Llamamos directamente
funcion_a_decorar()  # Salida: Antes..., ¡Función original...!, Después...
Python

Con @mi_decorador justo arriba de la definición, Python aplica el decorador automáticamente. Es idéntico al ejemplo anterior, pero más elegante. Prueba ejecutándolo—verás el mismo resultado. Recuerda: el decorador debe retornar una función callable.

Decoradores con argumentos: añadiendo flexibilidad

Hasta ahora, nuestros ejemplos asumían funciones sin argumentos. Pero los decoradores pueden manejar funciones con parámetros. Solo ajusta el envoltorio para pasar *args y **kwargs, que capturan cualquier argumento posicional o nombrado.

Analogía: es como un envoltorio universal que se adapta a cualquier tamaño de regalo.

# Decorador que maneja argumentos
def mi_decorador(funcion_original):
    def envoltorio(*args, **kwargs):
        print("Antes con argumentos:", args, kwargs)
        resultado = funcion_original(*args, **kwargs)  # Pasamos los args
        print("Después")
        return resultado  # Retornamos el resultado original
    return envoltorio

@mi_decorador
def suma(a, b):
    return a + b

print(suma(3, 5))  # Salida: Antes..., 8, Después...
Python

Aquí, el decorador imprime argumentos, ejecuta la original y retorna su valor. Esto lo hace versátil para funciones reales.

Aplicaciones prácticas: por qué usar decoradores

Los decoradores brillan en escenarios como logging, medición de tiempo o validación—sin modificar el código original. Por ejemplo, un decorador para medir tiempo:

import time

def mide_tiempo(funcion_original):
    def envoltorio(*args, **kwargs):
        inicio = time.time()
        resultado = funcion_original(*args, **kwargs)
        fin = time.time()
        print(f"Tiempo de ejecución: {fin - inicio} segundos")
        return resultado
    return envoltorio

@mide_tiempo
def funcion_lenta():
    time.sleep(1)
    print("¡Terminé!")

funcion_lenta()  # Salida: ¡Terminé!, Tiempo de ejecución: ~1 segundo
Python

Este mide el tiempo sin alterar funcion_lenta. Úsalo para optimizar código real—te da control sin invasión.

Errores comunes y cómo evitarlos

Un error típico es olvidar retornar el resultado en el envoltorio, lo que hace que el decorador devuelva None. Siempre verifica: llama a la original y retorna su salida.

Otro: no manejar argumentos, causando errores en funciones con params. Usa *args, **kwargs siempre para robustez.

Práctica con estos para internalizarlos—prueba decorar tus propias funciones y depura errores paso a paso.

Resumen del capítulo

  • Funciones como objetos de primera clase: En Python, las funciones se tratan como objetos manipulables, permitiendo asignarlas, pasarlas y retornarlas—base de los decoradores.
  • Funciones anidadas y retornadas: Aprendimos a definir funciones dentro de otras y retornarlas, creando “fábricas” de lógica personalizada.
  • Decoradores simples: Son funciones que envuelven otras, añadiendo comportamiento sin modificar el original, usando un envoltorio que se retorna.
  • Sintaxis @: Un atajo elegante para aplicar decoradores directamente sobre la definición de la función.
  • Manejo de argumentos: Usa *args y **kwargs para decoradores versátiles que funcionen con cualquier firma de función.
  • Aplicaciones y errores: Explora usos como logging o timing, y evita trampas comunes como olvidar retornar resultados para dominar esta herramienta poderosa.

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio