Los decoradores son funciones que toman otra función como argumento con el fin de extender o reemplazar el comportamiento de la función que reciben.
Si la función A es un decorador y la función B es su argumento, entonces la función A modifica, extiende o reemplaza el comportamiento de la función B sin modificar el código de la función B.
Decimos que la función decoradora A envuelve a la función B.
Aunque hablemos de «modificar» el comportamiento, la función envuelta no cambia en realidad.
El comportamiento se añade alrededor de la función envuelta (y de lo que devuelve), o bien el comportamiento de la función envuelta se sustituye por otro comportamiento.
Una función de orden superior es una función que acepta una o más funciones como argumentos y/o devuelve una o más funciones. Una función, usada como argumento o devuelta por otra función, es una función de primera clase. Una función de Python, como objeto invocable, es una función de primera clase que se puede almacenar en una variable o usar como argumento, igual que cualquier otro valor u objeto. Las funciones de orden superior y las funciones de primera clase trabajan juntas para hacer posibles los decoradores.
El símbolo @ se antepone al nombre de la función decoradora y se coloca justo encima de la función que se va a decorar, así:
@decorator
def decorated_function():
pass
Algunos decoradores aceptan argumentos:
@decorator_with_arg(name="Bob")
def decorated_function2():
pass
Si un decorador tiene argumentos predeterminados definidos, debes usar paréntesis en la llamada @decorator() para que el decorador funcione, igual que al llamar a cualquier función:
@decorator_with_default_arg()
def decorated_function3():
pass
Si un decorador toma argumentos posicionales, no proporcionar los argumentos dará lugar a un error parecido a:
TypeError: decorator_with_pos_arg() missing 1 required positional argument: 'name'
La sintaxis @decorator es azúcar sintáctico o una forma abreviada de llamar a la función decoradora y pasarle la función decorada como argumento.
A continuación se muestran ejemplos de formas alternativas de llamar a un decorador:
def function():
pass
function = decorator(function)
def function2():
pass
function2 = decorator_with_arg(name="Bob")(function2)
def function3():
pass
function3 = decorator_with_default_arg()(function3)
La mayoría de los decoradores están pensados para extender o reemplazar el comportamiento de otra función, pero algunos decoradores pueden limitarse a devolver las funciones que envuelven.
Los decoradores son funciones que toman al menos un argumento: la función que envuelven. Normalmente devuelven la función envuelta o el resultado de una expresión que usa la función envuelta.
Un decorador sencillo, uno que simplemente devuelve su función envuelta, se puede escribir así:
>>> def do_nothing(func):
... return func
...
... @do_nothing
... def function4():
... return 4
...
>>> print(function4())
4
Un decorador puede limitarse a añadir efectos secundarios, como información adicional que se usa para el registro:
>>> def my_logger(func):
... print(f"Entering {func.__name__}")
... return func
...
... @my_logger
... def my_func():
... print("Hello")
...
>>> my_func()
Entering my_func
Hello
Un decorador no se devuelve a sí mismo. Puede devolver sus argumentos de función, otra función, o uno o más valores que sustituyen el valor devuelto por la función recibida o decorada. Si un decorador devuelve otra función, normalmente devolverá una función interna.
Una función se puede definir dentro de otra función. Esa función anidada se llama función interna. Un decorador puede usar una función interna para envolver su argumento de función. El decorador entonces devuelve su función interna. La función interna puede luego devolver el argumento de función original.
A continuación se muestra un ejemplo de un decorador usado para validar:
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
@my_validator
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Pluto is not a planet!
En la primera línea tenemos la definición del argumento func del decorador.
En la línea siguiente está la definición de la función interna del decorador, que envuelve el argumento func.
Como la función interna envuelve el argumento func del decorador, se le pasa el mismo argumento que se pasa a func.
Fíjate en que la envoltura no tiene que usar el mismo nombre para el argumento que se definió en func.
La función original usa planet y el decorador usa world, y el decorador sigue funcionando.
La función interna devuelve func o, si world == "Pluto", imprime que Plutón no es un planeta.
En su lugar, se podría programar para que lance un ValueError.
Así pues, la función interna envuelve func y devuelve func o hace algo que sustituye lo que haría func.
El decorador devuelve su función interna.
La función interna puede devolver o no la función original que se le pasó.
Según el código que se ejecute condicionalmente en la función de envoltura o en la función interna, se puede devolver func, se puede lanzar un error, o se puede devolver un valor del tipo devuelto por func.
Se pueden escribir decoradores para funciones que toman un número arbitrario de argumentos usando la sintaxis *args y **kwargs.
A continuación se muestra un ejemplo de un decorador para una función que toma un número arbitrario de argumentos:
def double(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper
@double
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 18
print(add(2, 3, 4, 5, 6))
#-> 40
Esto sirve para duplicar el valor devuelto por la función argumento. Si queremos triplicar, cuadruplicar, etc. el valor devuelto, podemos añadir un parámetro al propio decorador, como mostramos en la sección siguiente.
A continuación se muestra un ejemplo de un decorador que se puede configurar para multiplicar el valor devuelto por la función decorada por una cantidad arbitraria:
def multi(factor=1):
if factor == 0:
raise ValueError("factor must not be 0")
def outer_wrapper(func):
def inner_wrapper(*args, **kwargs):
return func(*args, **kwargs) * factor
return inner_wrapper
return outer_wrapper
@multi(factor=3)
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 27
print(add(2, 3, 4, 5, 6))
#-> 60
Las primeras líneas validan que factor no sea 0.
Después se define la envoltura externa.
Esta tiene la misma firma que esperamos para un decorador sin parámetros.
La envoltura externa tiene una función interna con la misma firma que la función original.
La envoltura interna se encarga de multiplicar el valor devuelto por la función original por el argumento que se pasa al decorador.
La envoltura externa devuelve la envoltura interna, y el decorador devuelve la envoltura externa.
A continuación se muestra un ejemplo de un decorador con parámetros que controla si valida o no el argumento que se pasa a la función original:
def check_for_pluto(check=True):
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if check and world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
return my_validator
@check_for_pluto(check=False)
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Hello, Pluto!
Esto permite alternar fácilmente entre comprobar Pluto o no, y se hace sin tener que modificar my_func.