Skip to content

📙 Clase 26 — Decoradores

Fase 5 · Python intermedio / "pro" ⬅️ Volver al índice de clases

🎯 Qué aprendí

  • Un decorador envuelve una función para agregarle comportamiento sin tocarla.
  • La anatomía: función que recibe función y devuelve función (+ functools.wraps).
  • Ya usaba dos sin saberlo: @property y @staticmethod.

📖 PARTE TEÓRICA

🎁 1. La idea: envolver una función

En Python las funciones son valores: se pasan como argumento y se devuelven (Clase 24). Un decorador aprovecha eso: recibe tu función y devuelve una versión envuelta.

python
import functools, time

def cronometrar(funcion):                     # ① recibe la función original
    @functools.wraps(funcion)                 #    (conserva su nombre/doc)
    def envoltura(*args, **kwargs):           # ② la envoltura acepta lo que sea (Clase 09)
        inicio = time.perf_counter()
        resultado = funcion(*args, **kwargs)  # ③ llama a la original…
        ms = (time.perf_counter() - inicio) * 1000
        print(f"[{funcion.__name__} tardó {ms:.1f} ms]")   # ④ …más lo extra
        return resultado                      # ⑤ y devuelve su resultado
    return envoltura                          # ⑥ entrega la versión envuelta

@cronometrar                                  # ← "decora" a la función de abajo
def sumar_millon():
    return sum(range(1_000_000))

sumar_millon()
# [sumar_millon tardó 8.3 ms]     ← comportamiento agregado SIN tocar la función

El @ es puro azúcar sintáctico:

python
@cronometrar          # esto…
def f(): ...
# …es EXACTAMENTE lo mismo que:
f = cronometrar(f)    # reemplazar f por su versión envuelta
   llamada a sumar_millon()


   ┌─ envoltura ────────────────┐
   │ ⏱ medir inicio             │
   │   → sumar_millon original  │
   │ ⏱ medir fin + print        │
   └────────────────────────────┘

📌 @functools.wraps(funcion) dentro del decorador conserva el __name__ y docstring de la original (sin él, toda función decorada "se llamaría" envoltura — un dolor al depurar).

🛡️ 2. Decoradores con lógica: validar antes de ejecutar

python
def requiere_datos(funcion):
    @functools.wraps(funcion)
    def envoltura(datos, *args, **kwargs):
        if not datos:
            return "⚠️ No hay datos"      # corta ANTES de llamar a la original
        return funcion(datos, *args, **kwargs)
    return envoltura

@requiere_datos
def promedio(datos):
    return sum(datos) / len(datos)

print(promedio([2, 4]))   # 3.0
print(promedio([]))       # ⚠️ No hay datos   ← ni siquiera entró (adiós ZeroDivisionError)

👀 3. Ya usabas decoradores

DecoradorDónde lo viste
@property / @x.setterClase 16 · Encapsulación
@staticmethodClase 18 · App como clase
@contextmanagerpróxima clase 😉

🧪 Tip de entrevista: "¿Qué es un decorador?" → Una función que recibe una función y devuelve otra que la envuelve, agregando comportamiento (log, timing, validación, caché) sin modificar la original. @d equivale a f = d(f).


🖥️ EN TU APP DE ESCRITORIO

Los decoradores eliminan el código repetido de tus callbacks:

python
def con_estado(mensaje_exito):
    """Decorador CON parámetro: envuelve callbacks y reporta en la barra de estado."""
    def decorador(metodo):
        @functools.wraps(metodo)
        def envoltura(self, *args, **kwargs):
            try:
                resultado = metodo(self, *args, **kwargs)
                self.status.configure(text=f"✅ {mensaje_exito}")
                return resultado
            except ValueError as e:               # las validaciones de tus setters
                self.status.configure(text=f"⚠️ {e}")
        return envoltura
    return decorador

class App(ctk.CTk):
    @con_estado("Tarea agregada")
    def _agregar(self):
        self.agregar_tarea(self.entry.get())     # si lanza ValueError → status ⚠️

    @con_estado("Exportado")
    def _exportar(self):
        ...

Antes, cada callback repetía su try/except + mensaje. Ahora esa política vive en un lugar. Otros usos en apps: @requiere_conexion, @confirmar("¿Seguro?"), @log_accion.

💡 Bonus de rendimiento gratis: @functools.lru_cache memoriza resultados de funciones puras — llamas dos veces con los mismos argumentos y la segunda es instantánea.


🗄️ CON BASE DE DATOS (caso de uso)

El patrón "abrir conexión → operar → commit/rollback → cerrar" se repite en cada operación. Un decorador lo centraliza:

python
def con_bd(funcion):
    @functools.wraps(funcion)
    def envoltura(*args, **kwargs):
        con = sqlite3.connect("app.db")
        try:
            resultado = funcion(con, *args, **kwargs)   # le INYECTA la conexión
            con.commit()
            return resultado
        finally:
            con.close()
    return envoltura

@con_bd
def agregar_contacto(con, nombre, tel):
    con.execute("INSERT INTO contactos VALUES (?, ?)", (nombre, tel))

agregar_contacto("Ana", "999-111")     # conexión, commit y cierre: automáticos

🏋️ EJERCICIOS CON SOLUCIÓN

Ejercicio 1 — Anunciador

Escribe @anunciar que imprima ">> llamando f" antes de ejecutar la función f.

Ver solución
python
import functools

def anunciar(funcion):
    @functools.wraps(funcion)
    def envoltura(*args, **kwargs):
        print(f">> llamando {funcion.__name__}")
        return funcion(*args, **kwargs)
    return envoltura

@anunciar
def saludar():
    return "hola"

print(saludar())
# >> llamando saludar
# hola

Ejercicio 2 — Reintentos

Escribe @reintentar que, si la función lanza excepción, la intente una segunda vez antes de rendirse.

Ver solución
python
import functools

def reintentar(funcion):
    @functools.wraps(funcion)
    def envoltura(*args, **kwargs):
        try:
            return funcion(*args, **kwargs)
        except Exception:
            return funcion(*args, **kwargs)    # segundo intento (si falla, propaga)
    return envoltura

Ejercicio 3 — Descífralo

Sin correr el código: ¿qué imprime f(3)?

python
def duplicador(funcion):
    def envoltura(x):
        return funcion(x) * 2
    return envoltura

@duplicador
def f(x):
    return x + 1
Ver solución
python
# f(3) → envoltura(3) → funcion(3)*2 → (3+1)*2 → 8

❓ Preguntas y respuestas (autoevaluación)

1. ¿A qué equivale exactamente @decorador sobre def f?

A f = decorador(f): reemplazar f por la versión envuelta que devuelve el decorador.

2. ¿Por qué la envoltura se define con *args, **kwargs?

Para envolver cualquier función sin importar su firma, y pasarle todo tal cual.

3. ¿Qué aporta functools.wraps?

Conserva __name__, docstring y metadatos de la función original en la envuelta.

4. Nombra dos decoradores que ya usabas antes de esta clase.

@property (y @x.setter) y @staticmethod.


📎 Apuntes relacionados

➡️ Siguiente

Clase 27 · Type hints — anotar tipos para código autoexplicado.