Blog

Decoradores en Python, explicados

Un decorador es una función que toma una función y devuelve un reemplazo. La línea con @ es la forma corta de reasignar el nombre.

Un decorador es una función que toma una función y devuelve un reemplazo para ella. La línea con @ es la forma corta de reasignar el nombre.

Una vez que escribes uno a mano, la sintaxis deja de parecer magia.

Escribe uno sin el @

El wrapper interior recibe cualquier cosa, llama al original, y cambia el resultado.

# a decorator is a function that returns a function
def shout(fn):
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs).upper()
    return wrapper

def greet(name):
    return f'hello {name}'

loud = shout(greet)
print(loud('ada'))

Imprime:

HELLO ADA

Todavía no pasó nada especial. Entró una función y salió una función distinta.

La línea con @ es lo mismo

@shout encima de una definición significa greet2 = shout(greet2).

# the @ syntax is the same thing
@shout
def greet2(name):
    return f'hello {name}'

print(greet2('ada'))

Imprime:

HELLO ADA

El mismo resultado. El decorador corre una sola vez, al definir.

El wrapper tapa al original

El nombre y el docstring ahora pertenecen al wrapper.

# the wrapper hides the original function
print(greet2.__name__, '|', greet2.__doc__)

Imprime:

wrapper | None

Esto rompe la salida de help, los depuradores y cualquier cosa que lea __name__.

functools.wraps lo arregla

@functools.wraps(fn) copia el nombre, el docstring y algunos atributos más al wrapper.

# functools.wraps keeps the metadata
import functools

def shout_fixed(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs).upper()
    return wrapper

@shout_fixed
def greet3(name):
    """Say hello."""
    return f'hello {name}'

print(greet3.__name__, '|', greet3.__doc__)
print(greet3('ada'))

Imprime:

greet3 | Say hello.
HELLO ADA

Agrégalo a todos los decoradores que escribas. No hay ningún caso en el que quieras los metadatos del wrapper.

Un decorador que recibe argumentos

@repeat(3) significa «llama a repeat(3), y usa lo que devuelva como decorador». Eso necesita un nivel más de anidamiento.

# a decorator that takes arguments needs one more layer
def repeat(times):
    def decorator(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            return [fn(*args, **kwargs) for _ in range(times)]
        return wrapper
    return decorator

@repeat(3)
def roll():
    return 4

print(roll())

Imprime:

[4, 4, 4]

Tres capas: la que recibe el argumento, el decorador, y el wrapper.

Guardar estado

El wrapper es un closure, así que puede guardar estado entre llamadas. Colgarlo del wrapper lo hace legible desde afuera.

# state in the closure, such as counting calls
def counted(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        return fn(*args, **kwargs)
    wrapper.calls = 0
    return wrapper

@counted
def work(n):
    return n * 2

work(1); work(2); work(3)
print('called', work.calls, 'times')

Imprime:

called 3 times

El que viene en la biblioteca estándar

functools.lru_cache es un decorador que cachea resultados según los argumentos. Sobre un Fibonacci recursivo ingenuo convierte trabajo exponencial en lineal.

# caching is a decorator in the standard library
@functools.lru_cache(maxsize=None)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(30), fib.cache_info())

Imprime:

832040 CacheInfo(hits=28, misses=31, maxsize=None, currsize=31)

La información del caché muestra cuántas llamadas se respondieron sin ejecutar el cuerpo.

Qué recordar

  • @decorador significa nombre = decorador(nombre).

  • El decorador corre al definir, el wrapper corre al llamar.

  • Usa siempre functools.wraps o pierdes el nombre y el docstring.

  • Un decorador con argumentos necesita tres funciones anidadas.

Casi todos los decoradores que vas a escribir son de logging, medición de tiempo, caché, reintentos o control de acceso. Todos tienen la misma forma que el primer ejemplo de esta página.

¿Qué tan útil te resultó este post?

¡Haz clic en un corazón para calificar!

Calificación promedio 0 / 5. Total de votos: 0

Todavía no hay votos. Sé el primero en calificar este post.