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
-
@decoradorsignificanombre = decorador(nombre). -
El decorador corre al definir, el wrapper corre al llamar.
-
Usa siempre
functools.wrapso 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.