Decoradores en Python (II) - Decoradores con parámetros

Este post fue migrado de un blog hecho con Wordpress. Si se ve mal, dejame un comentario y lo arreglo.

Un año después del primer artículo, llega el segundo. ¿Por qué tardó tanto? Por lo general mis artículos técnicos surgen de algún problema que se me presenta y para el cual necesito investigar antes de poder solucionarlo. El artículo anterior cubría todo lo que necesité hacer con decoradores en Python hasta el mes pasado, cuando necesité decoradores con parámetros.

Si no leíste el artículo anterior, te recomiendo que lo hagas antes de seguir: Decoradores en Python (I).

Decoradores con Parámetros

Cuando quise escribir un decorador con un parámetro me encontré con errores que ni siquiera entendía. No solo que los estaba escribiendo mal, sino que también los estaba usando mal. Te voy a evitar el sufrimiento.

Un decorador con parámetro se aplica así (siendo deco un decorador y 1 el argumento utilizado):

@deco(1)

def funcion_a_decorar(a, b, c):

    pass

Creo que la raíz de mi confusión fue el azúcar sintáctica (si, el @). Así que vamos a sacarlo y ver cómo se usaría este decorador en una versión de Python más vieja:

def funcion_a_decorar(a, b, c):

    pass
funcion_a_decorar = deco(1)(funcion_a_decorar)

Esto luce más claro para mi: deco es llamado con un argumento y el resultado tiene que ser algún objeto que pueda ser llamado con una función como parámetro para... decorarla. ¿Se entiende la idea? Vamos a definir deco, va a recibir un parámetro y utilizarlo para crear un decorador como los del artículo anterior. Finalmente retorna este decorador interno.

Agreguemos semántica al ejemplo. Mi decorador con parámetro recibirá un número, este número se usará para indicar cuantas veces queremos ejecutar la función decorada.

def deco(i):

    def _deco(f):

        def inner(*args, **kwargs):

            for n in range(i):

                r = f(*args, **kwargs)

            return r

        return inner

    return _deco

Como una convención personal, uso para el nombre de la segunda función _{nombre de la primer funcion}. Notemos entonces que _deco es un decorador dinámico, dependiendo del parámetro i, la función inner se compilará de una forma o de otra. Apliquemos el decorador:

@deco(2)

def saluda(nombre):

    print "hola", nombre
>>> saluda("juanjo")

hola juanjo

hola juanjo
@deco(3)

def suma1():

    global n

    n += 1
>>> n = 0

>>> suma1()

>>> n

3

Cuando aplicamos deco, se ejecuta deco, se compila _deco, se aplica _deco a la función que definimos y se compila inner utilizando un valor dado para i. Cuando llamamos a nuestra función (saluda, o suma1, en los ejemplos) se ejecuta inner.

¡Espero que se haya entendido!

Si no...

Si en lo anterior no fui lo suficientemente claro (por favor quejate en un comentario), no todo está perdido. Te puedo entregar un decorador para decoradores que convierte a tu decorador en un decorador con parámetros. ¿Qué tal?

def decorador_con_parametros(d):

    def decorador(*args, **kwargs):

        def inner(func):

            return d(func, *args, **kwargs)

        return inner

    return decorador

Original usando lambda en http://pre.activestate.com/recipes/465427/

Se usa así:

@decorador_con_parametros

def deco(func, i):

    def inner(*args, **kwargs):

        for n in range(i):

           r = func(*args, **kwargs)

        return r

    return inner
@deco(2)

def saludar(nombre):

    print "chau", nombre
>>> saludar("juanjo")

chau juanjo

chau juanjo

Para la próxima

Para el próximo artículo voy a explorar utilizar clases decoradoras en lugar de funciones decoradoras. Si bien todavía no lo terminé de investigar, me parece un enfoque que permite escribir código más organizado. Veremos! update: aquí está.

Comentarios

Comments powered by Disqus