Tracks
/
Python
Python
/
Temario
/
Decoradores
De

Decoradores en Python

{one: "1 ejercicio", many: "%{count} de ejercicios", other: "%{count} ejercicios"}

Acerca de Decoradores

Los decoradores son funciones que toman otra función como argumento con el propósito 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 agrega 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.

Un decorador es una función de orden superior

Una función de orden superior es una función que acepta una o más funciones como argumentos, que devuelve una o más funciones, o ambas cosas. Una función que se usa como argumento o que se devuelve desde 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 guardar 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.

Cómo se ve usar un decorador

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, tal como lo harías al llamar a cualquier función:

@decorator_with_default_arg()
def decorated_function3():
    pass

Si un decorador toma argumentos posicionales, no proporcionarlos dará como resultado 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 hay 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)

Cómo escribir un decorador simple

La mayoría de los decoradores están pensados para extender o reemplazar el comportamiento de otra función, pero algunos decoradores pueden no hacer nada más que devolver las funciones que envuelven.

Los decoradores son funciones que toman al menos un argumento: la función que están envolviendo. Por lo general, devuelven la función envuelta o el resultado de una expresión que usa la función envuelta.

Un decorador simple, uno que simplemente devuelve la función que envuelve, se puede escribir así:

>>> def do_nothing(func):
...     return func
...
... @do_nothing
... def function4():
...     return 4
...
>>> print(function4())
4

Un decorador puede limitarse a agregar efectos secundarios, como información adicional que sirve para llevar un 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 reemplazan el valor devuelto por la función que recibe o decora. Si un decorador devuelve otra función, por lo general devolverá una función interna.

Funciones internas

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. Luego, el decorador devuelve su función interna. A su vez, la función interna puede devolver el argumento de función original.

Un decorador de validación que usa una función interna

A continuación hay un ejemplo de un decorador que se usa 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, recibe el mismo argumento que se le pasa a func. Observa que la función envolvente 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 aun así el decorador funciona.

La función interna devuelve func o, si world == "Pluto", imprime que Plutón no es un planeta. En su lugar, podría codificarse para lanzar un ValueError. Así, la función interna envuelve a 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 de forma condicional en la función envolvente o en la función interna, puede ocurrir que se devuelva func, que se lance un error o que se devuelva un valor del tipo de retorno de func.

Decorar una función que toma una cantidad arbitraria de argumentos

Se pueden escribir decoradores para funciones que toman una cantidad arbitraria de argumentos usando la sintaxis *args y **kwargs.

A continuación hay un ejemplo de un decorador para una función que toma una cantidad arbitraria 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 de retorno de la función que se pasa como argumento. Si queremos triplicar, cuadruplicar, etc. el valor de retorno, podemos agregar un parámetro al propio decorador, como mostramos en la siguiente sección.

Decoradores que tienen sus propios parámetros

A continuación hay un ejemplo de un decorador que se puede configurar para multiplicar el valor de retorno de 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. Luego se define la función envolvente externa. Esta tiene la misma firma que esperamos para un decorador sin parámetros. La función envolvente externa tiene una función interna con la misma firma que la función original. La función envolvente interna se encarga de multiplicar el valor devuelto por la función original por el argumento que se pasa al decorador. La función envolvente externa devuelve la función envolvente interna, y el decorador devuelve la función envolvente externa.

A continuación hay 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 verificar que sea Pluto o no, y se hace sin tener que modificar my_func.

Editar en GitHub El enlace se abre en una ventana o pestaña nueva