Blog

Decoradores en Python, explicados

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.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.