Python: ¿qué es un decorador?

Publicado por

En general, un decorador es un patrón de software que se utiliza para alterar el funcionamiento de una determinada pieza de código; ya sea una función, o una clase, sin la necesidad de emplear otros mecanismos como la herencia.

En concreto, al hablar de los decoradores en Python, nos referimos a funciones u objetos con un comportamiento similar que nos permiten alterar cómo funcionan otras entidades sin tener que modificar su código explícitamente.

Para entender un poco mejor la idea, es necesario aclarar unas cuantas cosas acerca de las funciones. Las funciones son, para fines prácticos, objetos, y como tales pueden ser pasadas como parámetros, reasignadas, devueltas por otras funciones e incluso definidas dentro de funciones, es decir, pueden ser anidadas.

Toma el siguiente código como ejemplo:

def pares(numeros):
    def es_par(numero):
        return (numero % 2) == 0

    return list(filter(es_par, numeros))

Aquí se utiliza la función anidada es_par() para filtrar los números pares en una lista de valores arbitraria.

En el caso de los decoradores, las funciones anidadas nos sirven para desempeñar tareas alrededor de la ejecución de la rutina que deseamos decorar. Para ilustrar la situación te mostraré un decorador que toma un texto que consiste de varios renglones y a cada uno de estos le agrega como prefijo el número de línea que le corresponde. Empecemos con una función ordinaria que realiza esta tarea:

def agrega_numero_de_linea(texto):
    lineas = []

    for numero, linea in enumerate(texto.split('\n'), 1):
        lineas.append(f'{numero:6} {linea}')

    return '\n'.join(lineas)

De este modo, si quisiéramos tener líneas numeradas en un texto, basta con llamar esta función:

def diccionario_a_texto(diccionario):
    texto = ''
    for llave, valor in diccionario.items():
        texto += f'{llave:12}: {valor}\n'
    return texto

juan = {
    'nombre': 'Juan Pérez',
    'edad': 25
}
print(diccionario_a_texto(juan))

nombre      : Juan Pérez
edad        : 25

print(agrega_numero_de_linea(diccionario_a_texto(juan)))

     1 nombre      : Juan Pérez
     2 edad        : 25
     3

Sin embargo, esto no se ve muy elegante, además de que reutilizar la función agrega_numero_de_linea() no resulta tan sencillo. Un decorador nos ayudará a ocultar el código que numera la línea y a aprovecharla en otras funciones de una manera fácil y elegante:

def con_linea_numerada(funcion):

    def agrega_numero_de_linea(*args, **kwargs):
        resultado = funcion(*args, **kwargs)
        lineas = []

        for numero, linea in enumerate(str(resultado).split('\n'), 1):
            lineas.append(f'{numero:6} {linea}')

        return '\n'.join(lineas)

    return agrega_numero_de_linea

La función de arriba es un decorador, la cual recibe como argumento otra definida en el exterior, y devuelve una tercera rutina definida internamente, una función anidada. La parte interesante es que la función anidada invoca a la externa y posteriormente procesa dicho resultado, agregando la funcionalidad del decorador al resultado final. Una vez definido el decorador, podemos utilizarlo de la siguiente manera:

def diccionario_a_texto(diccionario):
    texto = ''
    for llave, valor in diccionario.items():
        texto += f'{llave:12}: {valor}\n'
    return texto

diccionario_a_texto = con_linea_numerada(diccionario_a_texto)

juan = {
    'nombre': 'Juan Pérez',
    'edad': 25
}

print(diccionario_a_texto(juan))

     1 nombre      : Juan Pérez
     2 edad        : 25
     3

Nota, como inmediatamente después de definir la función diccionario_a_texto, esta es redefinida utilizando el decorador. Esta instrucción es tan común al utilizar decoradores, que Python ha definido un poco de azúcar sintáctica para representarla: simplemente hay que colocar el nombre del decorador, con un arroba como prefijo, antes de la definición de la función que quieres decorar, en nuestro caso, la definición se verá así:

@con_linea_numerada
def diccionario_a_texto(diccionario):
    texto = ''
    for llave, valor in diccionario.items():
        texto += f'{llave:12}: {valor}\n'
    return texto

juan = {
    'nombre': 'Juan Pérez',
    'edad': 25
}

print(diccionario_a_texto(juan))

     1 nombre      : Juan Pérez
     2 edad        : 25
     3

Esto hace que reutilizar el decorador sea todavía más sencillo:

@con_linea_numerada
def muestra_archivo(nombre_de_archivo):
    with open(nombre_de_archivo) as archivo:
        return archivo.read()

Como puedes darte cuenta, los decoradores son muy útiles para reutilizar código que desempeña tareas comunes, las posibilidades son muy variadas, desde dar formato a texto, como en el ejemplo que te he mostrado, sincronizar tareas en paralelo, o manejar errores. El límite es nuestra creatividad. Espero que esta nota te haya servido y que le saques el máximo provecho a esta herramienta. Nos leemos en la siguiente nota ¡Hasta entonces!

One comment

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *