W3docs

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, PYTHON

wrapper 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     — lost

Soluciona 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 8

Timer

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  499999500000

time.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))  # 832040

Para 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
# hello

Leyendo 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)  # 2

functools.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 wrapper

La 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ónCuándo usarlo
wrapper básicoAñadir comportamiento antes/después de una función
@functools.wrapsSiempre — preserva __name__, __doc__
Fábrica de decoradores (3 niveles)Necesitas configurar el decorador
Decoradores apiladosComponer múltiples comportamientos independientes
Decorador basado en claseNecesitas estado persistente entre llamadas
@functools.lru_cacheMemoizar funciones puras (incorporado, listo para producción)

Práctica

Práctica
What does @functools.wraps(func) do inside a decorator?
What does @functools.wraps(func) do inside a decorator?
Práctica
Given @bold applied above @italic on a function, which decorator is applied first?
Given @bold applied above @italic on a function, which decorator is applied first?
Práctica
What is a decorator factory?
What is a decorator factory?
Was this page helpful?