Decoradores en Python
Aprende cómo funcionan los decoradores en Python: escribe los tuyos, preserva metadatos con functools.wraps, apílalos y descubre casos de uso reales.
Un decorador es una función que envuelve a otra función para ampliar o modificar su comportamiento sin cambiar su código fuente. Los decoradores son una de las características más poderosas e idiomáticas de Python — son el motor detrás de @staticmethod, @classmethod, @property, @functools.lru_cache y muchos patrones populares en frameworks web.
Esta página cubre cómo funcionan los decoradores, cómo escribir los tuyos desde cero, cómo pasarles argumentos, cómo apilarlos y cuándo es más útil cada patrón.
Cómo funcionan los decoradores
Un decorador es simplemente una función que toma otra función como argumento y devuelve una función nueva. Python proporciona la sintaxis @ como una forma abreviada de aplicar uno:
@shout
def greet(name):
return f"hello, {name}"Esto es exactamente equivalente a:
def greet(name):
return f"hello, {name}"
greet = shout(greet)La línea @shout le dice a Python: después de definir greet, pásalo inmediatamente a shout y reasigna el nombre greet a lo que devuelva shout. A partir de ese momento, cada llamada a greet(...) pasa primero por la lógica de shout.
Escribiendo tu primer decorador
Un decorador normalmente define una función envolvente interna que llama a la función original y añade comportamiento adicional a su alrededor:
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@shout
def greet(name):
return f"hello, {name}"
print(greet("world")) # HELLO, WORLD
print(greet("python")) # HELLO, PYTHONwrapper acepta *args y **kwargs para reenviar cualquier combinación de argumentos a func sin modificarlos. Esto hace que el decorador sea compatible con cualquier función independientemente de su firma — un buen hábito desde el principio.
Por qué el envolvente debe devolver la función interna
shout termina con return wrapper, no con return wrapper(). Esto es intencional: shout está construyendo un nuevo callable, no llamándolo todavía. Si accidentalmente escribieras return wrapper(), el decorador se ejecutaría inmediatamente en el momento de la decoración y greet quedaría vinculado al valor de retorno de wrapper — una cadena — en lugar del callable en sí.
Preservar metadatos con functools.wraps
Cada función de Python lleva metadatos: __name__, __doc__, __module__ y más. Sin el cuidado necesario, un decorador reemplaza la función original con wrapper, perdiendo todos esos datos:
def shout(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
"""Say hello to name."""
return f"hello, {name}"
print(greet.__name__) # wrapper — wrong
print(greet.__doc__) # None — lostSoluciona esto aplicando @functools.wraps(func) al envolvente. Copia los metadatos de la función original sobre wrapper:
import functools
def shout(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@shout
def greet(name):
"""Say hello to name."""
return f"hello, {name}"
print(greet("world")) # HELLO, WORLD
print(greet.__name__) # greet
print(greet.__doc__) # Say hello to name.Usa siempre @functools.wraps en cualquier decorador que escribas. Sin él, las herramientas de depuración, los generadores de documentación y los frameworks de prueba ven el nombre de función incorrecto. La única excepción es cuando intencionalmente quieres ocultar la identidad original.
Ejemplos prácticos de decoradores
Logger
Registra cada llamada a una función con sus argumentos y valor de retorno:
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(3, 5)
# Calling add with args=(3, 5) kwargs={}
# add returned 8Timer
Mide cuánto tarda en ejecutarse una función:
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.6f}s")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
total = slow_sum(1_000_000)
print(total) # slow_sum took 0.01xxs then 499999500000time.perf_counter() es la opción adecuada aquí porque tiene la mayor resolución disponible para mediciones de corta duración.
Memoización (caché)
Almacena en caché el valor de retorno para cada conjunto único de argumentos, de modo que la función nunca se calcule dos veces para la misma entrada:
import functools
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # 55
print(fibonacci(30)) # 832040Para código en producción, prefiere el incorporado @functools.lru_cache o @functools.cache (Python 3.9+), que gestionan los casos extremos, la seguridad en hilos y los límites del tamaño de caché. La versión manual anterior es útil para entender el patrón.
Control de acceso
Protege una función para que solo pueda ejecutarse cuando se cumpla una condición:
import functools
def require_auth(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("is_authenticated"):
raise PermissionError("Authentication required.")
return func(user, *args, **kwargs)
return wrapper
@require_auth
def get_dashboard(user):
return f"Welcome, {user['name']}!"
guest = {"name": "Guest", "is_authenticated": False}
admin = {"name": "Admin", "is_authenticated": True}
try:
print(get_dashboard(guest))
except PermissionError as e:
print(e) # Authentication required.
print(get_dashboard(admin)) # Welcome, Admin!Decoradores con argumentos
A veces necesitas configurar un decorador en el momento de la decoración — por ejemplo, para repetir una función un número variable de veces. Los decoradores simples no pueden recibir argumentos adicionales directamente porque Python pasa la función, no los argumentos. La solución es una fábrica de decoradores: una función que acepta la configuración y devuelve un decorador:
import functools
def repeat(n):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say(message):
print(message)
say("hello")
# hello
# hello
# helloLeyendo de afuera hacia adentro: @repeat(3) primero llama a repeat(3), que devuelve decorator. Python luego aplica decorator a say, que devuelve wrapper. Así, say termina apuntando a wrapper — el mismo patrón que antes, con el nivel extra solo para llevar n al ámbito.
El anidamiento puede parecer intimidante al principio. Un atajo mental: la función más externa contiene la configuración, la función intermedia contiene la función que se decora y la función más interna contiene la llamada que se intercepta.
Apilar múltiples decoradores
Puedes aplicar varios decoradores a una sola función apilando líneas @. Python los aplica de abajo hacia arriba — el decorador más cercano al def se aplica primero:
import functools
def bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "<b>" + func(*args, **kwargs) + "</b>"
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "<i>" + func(*args, **kwargs) + "</i>"
return wrapper
@bold
@italic
def greet(name):
return f"Hello, {name}"
print(greet("Alice")) # <b><i>Hello, Alice</i></b>Equivalente a greet = bold(italic(greet)). italic envuelve a greet primero, luego bold envuelve el resultado. La salida muestra que italic actúa más cerca de la cadena sin procesar y bold envuelve el exterior.
Decoradores basados en clases
Una clase también puede ser un decorador — cualquier objeto con un método __call__ es callable. Los decoradores basados en clases son útiles cuando el decorador en sí necesita mantener estado entre llamadas:
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Call #{self.count} to {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello()
say_hello()
print(say_hello.count) # 2functools.update_wrapper(self, func) hace el mismo trabajo que @functools.wraps — copia los metadatos de la función original sobre la instancia. Tras la decoración, say_hello es una instancia de CountCalls, por lo que say_hello.count es un acceso a atributo normal.
Cuándo elegir una clase en lugar de un decorador función:
- Necesitas estado persistente (
count,cache, flags). - El decorador tiene múltiples métodos o lógica auxiliar.
- Necesitas que el objeto decorado sea introspectable como un tipo específico.
Errores comunes con decoradores
Olvidar llamar a la función decorada
Un error frecuente al principio es devolver el envolvente pero olvidar llamar a func dentro de él:
def broken(func):
def wrapper(*args, **kwargs):
print("before")
# forgot to call func!
return wrapperLa función decorada devuelve None silenciosamente cada vez. Asegúrate siempre de que wrapper llame a func(*args, **kwargs) y devuelva su resultado.
Decorar en la capa incorrecta
Con decoradores parametrizados, olvidar la llamada externa es un error frecuente:
# Wrong — 'repeat' receives the function, not a count
@repeat # should be @repeat(3)
def say(msg):
print(msg)Esto pasa say a repeat donde se espera n, lo que provoca un TypeError al llamar a say.
El orden de los decoradores importa
Con decoradores apilados, el orden cambia el comportamiento. @timer seguido de @log_calls en la misma función medirá el tiempo de la versión ya registrada, mientras que a la inversa registrará la versión ya cronometrada. Piensa bien qué quieres que vea cada capa.
Relación con los cierres
La función wrapper de un decorador es un cierre — captura func del ámbito envolvente y lo mantiene vivo incluso después de que la función decoradora externa haya retornado. Entender los cierres hace que el funcionamiento interno de los decoradores sea obvio: el objeto celda que contiene func es exactamente lo que permite a wrapper llamar a la función original mucho después de que shout(greet) haya completado.
Para la sintaxis de *args y **kwargs utilizada dentro de los envolventes, consulta el capítulo dedicado. Para las expresiones lambda que se combinan bien con los decoradores en patrones de orden superior, consulta el capítulo sobre lambda.
Referencia rápida
| Patrón | Cuándo usarlo |
|---|---|
wrapper básico | Añadir comportamiento antes/después de una función |
@functools.wraps | Siempre — preserva __name__, __doc__ |
| Fábrica de decoradores (3 niveles) | Necesitas configurar el decorador |
| Decoradores apilados | Componer múltiples comportamientos independientes |
| Decorador basado en clase | Necesitas estado persistente entre llamadas |
@functools.lru_cache | Memoizar funciones puras (incorporado, listo para producción) |