Apariencia
📙 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:
@propertyy@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ónEl @ 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
| Decorador | Dónde lo viste |
|---|---|
@property / @x.setter | Clase 16 · Encapsulación |
@staticmethod | Clase 18 · App como clase |
@contextmanager | pró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.
@dequivale af = 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_cachememoriza 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
# holaEjercicio 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 envolturaEjercicio 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 + 1Ver 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): reemplazarfpor 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.