Decoradores en Python

En la anterior entrada, Closures , defino los closures en Python y muestro ejemplos; en esta entrada, Decoradores en Python, presentaré los decoradores con dos ejemplos.

Según Wikipedia, definimos el patrón Decorator como sigue:

El patrón Decorator responde a la necesidad de añadir dinámicamente funcionalidad a un Objeto. Esto nos permite no tener que crear sucesivas clases que hereden de la primera incorporando la nueva funcionalidad, sino otras que la implementan y se asocian a la primera.

En Python podemos definir clases decoradoras las cuáles añaden funcionalidad a otras clases; pero, cuando hablamos de decoradores en Python, nos referimos a funciones decoradoras que son utilizadas por otras funciones mediante el empleo de anotaciones.

Para comprender el concepto de decorador es necesario entender el concepto de Closures ya que un decorador es un closure al cual se le pasa la referencia de la función sobre la que se ejecuta y, si fuere necesario, parámetros de entrada.

En los siguientes apartados, muestro ejemplos de decoradores: el primero, un decorador básico con un registro de las funciones clientes; y, en el segundo, un decorador al que se le pasan parámetros.

Un decorador está formado por una función que en su interior puede tener la definición de otras funciones. En tiempos de ejecución, al cargar el módulo donde se encuentra la función cliente con la anotación del decorador, se ejecuta el contenido de la primera función del decorador, ejecutándose la función del decorador con la referencia a la función cliente cuando se produce la invocación de la función cliente con el decorador.

Ejemplo básico

Supongamos que queremos cuantificar las funciones decoradoras que son ejecutadas. Para ello, definimos un módulo con una función decoradora y una lista para que almacene las funciones que son ejecutadas. El resultado es el siguiente: módulo my_decorator_ej1.py en el cuál definimos la función decoradora:

#my_decorators_ej1.py
func_registry = []

def my_decorator_ej1(func):

    print(f'Entramos en my_decorator_ej1')

    def inner():
        print('Ejecutamos función (%s)' % func)
        result = func()
        func_registry.append(func)

        return result

    return inner

Supongamos el siguiente módulo cliente que hace uso de la función decoradora definida anteriormente:

#example1.py
from my_decorators_ej1 import my_decorator_ej1, func_registry

@my_decorator_ej1
def function2() -> None:
    print('Entramos en función 2')

@my_decorator_ej1
def function1() -> None:
    print('Entramos en función 1')
    function2()

def run() -> None:
    function1()
    print(f'Funciones ejecutadas...{len(func_registry)}')
    for fun in func_registry:
        print(fun)

Al ejecutar el snippet de código anterior, lo primero que se realizar es la carga del módulo de los decoradores; y, al cargarlos, se escribe por consola el mensaje ‘Entramos en my_decorator_ej1’ de la primera función del decorador; cuando se ejecutan las funciones clientes, se ejecutan las funciones decoradoras.

La salida por consola del ejemplo anterior es la siguiente:

Entramos en my_decorator_ej1
Entramos en my_decorator_ej1
Ejecutamos función (<function function1 at 0x10d40cee0>)
Entramos en función 1
Ejecutamos función (<function function2 at 0x10d40c940>)
Entramos en función 2
Funciones ejecutadas...2
<function function2 at 0x10d40c940>
<function function1 at 0x10d40cee0>

Función decoradora con parámetros

En el siguiente ejemplo vamos a suponer que deseamos mostrar por consola las funciones que son llamadas, los parámetros y su tiempo de ejecución. El patrón del mensaje por consola es un valor por defecto pero este puede ser diferente en función del patrón definido en la anotación del decorador.

La definición del decorador del ejemplo es la siguiente:

#my_decorators_ej1.py
DEFAULT_FMT = '[{end:0.8f}s] {name}({args}) Resultado={result}'
def my_decorator_ej2(fmt=DEFAULT_FMT):
    print(f'Entramos en my_decorator_ej2')

    def decorator(func):

        def print_console(*_args):
            start = time.time()
            _result = func(*_args)
            end = time.time() - start

            name = func.__name__
            result = repr(_result)
            args = ', '.join(repr(arg) for arg in _args)
            print(fmt.format(**locals()))

            return _result

        return print_console

    return decorator

La definición de la clase cliente que usa el módulo de la función decoradora es la siguiente:

from my_decorators_ej1 import my_decorator_ej2

@my_decorator_ej2()
def function1(param: str) -> int:
    print('Ejecuto function1()...')
    return 33

@my_decorator_ej2()
def function2(param: str) -> None:
    print('Ejecuto function2()...')

@my_decorator_ej2('{name}({args}) => {result}')
def function3(param: str) -> int:
    print('Ejecuto function3()...')
    return 66


def run() -> None:
    function1('param1')

    function2('param1')

    function3('param1')

La salida por consola de la ejecución del snippet anterior es la siguiente:

Entramos en my_decorator_ej2
Entramos en my_decorator_ej2
Entramos en my_decorator_ej2
Ejecuto function1()...
[0.00003290s] function1('param1') Resultado=33
Ejecuto function2()...
[0.00002384s] function2('param1') Resultado=None
Ejecuto function3()...
function3('param1') => 66

De la ejecución anterior los primeros mensajes que se muestran en la consola son los correspondientes a los mensajes que se escriben en la primera función del decorador; los siguientes mensajes, son los correspondientes a la ejecución de cada función cliente en función del patrón definido en la anotación del decorador.

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s