Automágica: durante 2017 estoy trabajando bastante en Automágica, mi software para editar libros: Más información - Posts relacionados

Charla: Entendiendo Decoradores en Python [actualizada]

La primer vez que di esta charla fue en el PyDay de Rafala en 2010. De las charlas que tengo en la gatera, es la que más me gusta de momento. Está bien armada y es muy útil para quienes empiezan con Python. Por eso cuando me invitaron a dar una charla en el PyDay de Córdoba, no dude en presentarla.

Dejo on line la versión actualizada (y sus fuentes) ya que los estuve buscando antes de dar la charla y me costó encontrarlos en mi computadora. Subiéndolas a Internet aumento su disponibilidad.

Esta charla también fue dada en PyCon Argentina 2010.


Video de la charla Entendiendo decoradores en Python

Orfi se tomó el trabajo de editar una filmación de mi charla en el PyDay de Rafaela con mis slides para armar este video. Muchas gracias!

entendiendo decoradores from Orfx Sch on Vimeo.

Al final, durante las preguntas, escribo algo de código Python en la terminal. Lo siguiente es una reproducción:

>>> def f(a, b):

...     print a, b

...

>>> f(1, 2)

1 2

>>> def f(*a, **kw):

...     print a

...     print kw

...

>>> f(1)

(1,)

{}

>>> f(1, parametro=2)

(1,)

{'parametro': 2}

>>> def f(p1, *a, **kw):

...     print kw['param']

...

>>> f()

Traceback (most recent call last):

  File "", line 1, in

TypeError: f() takes at least 1 argument (0 given)

>>> f(1, 2, 3, param=0)

0

>>> def deco(f):

...     def _deco(*a, **kw):

...             if kw.get('p'):

...                     return f(*a, **kw)

...             else:

...                     print "No ejecuto."

...     return _deco

...

>>> @deco

... def saludo(*a, **kw):

...     print "hola"

...

>>> saludo()

No ejecuto.

>>> saludo(p=0)

No ejecuto.

>>> saludo(p=1)

hola


Decorando decoradores

Una de las preguntas que aveces me hacen luego de hablar sobre Taint Mode es: una vez que la aplicación pasa de desarrollo a producción, ¿hay alguna forma de deshabilitar los decoradores?

La biblioteca usa decoradores para marcar entradas no confiables, sumideros sensibles y funciones limpiadoras con la idea de encontrar posibles vulnerabilidades en tiempo de desarrollo. Si en producción no los requerimos más, esos decoradores producen overhead.

Entonces... qué se puede hacer? Editar el código comentando los decoradores no escala[0]. Una alternativa es utilizar un nuevo decorador: llamemoslo @apply_decorator y vamos a utilizarlo para controlar mediante alguna condición (por ejemplo una variable en el archivo de configuración) si se debe usar o no el decorador.

COND = True

def apply_decorator(d):

if COND:

    return d

else:

    return lambda f: f

Si la condición es verdadera, se retorna el decorador original, sino una función fake (implementada utilizando lambda) que recibe una función y retorna la misma función: un decorador que no hace nada.

Un ejemplo de su uso en el REPL de Python:


>>> @apply_decorator

... def mydeco(f):

...     def inner(*a, **kw):

...             print "decorado"

...             return f(*a, **kw)

...     return inner

... 

>>> mydeco



>>> COND = False

>>> @apply_decorator

... def mydeco(f):

...     def inner(*a, **kw):

...             print "decorado"

...             return f(*a, **kw)

...     return inner

... 

>>> mydeco

 at 0xb7753dbc>

Podemos aplicarlo directamente a la definición de nuestros decoradores


@apply_decorator

def mi_decorador(f):

    ...

o al principio de nuestro programa.


mi_decorador = apply_decorator(mi_decorador)

[0] nessita trademark.


functools.update_wrapper

Este post se alinea con la serie Decoradores en Python (I, II, III) pero no es tan elaborado como para ser Decoradores en Python (IV) :)

Desde Python 2.5, al crear un decorador, se puede utilizar functools.update_wrapper para quela versión decorada de la función, tenga los atributos name, doc, module y dict de la función original.

>>> import functools

>>> def deco(f):

... def inner(a, *kw):

... print "Este decorador no hace nada"

... return f(a, *kw)

... return inner

...

>>> def saludo():

... print "hola"

...

>>> saludo2 = deco(saludo)

>>> saludo2()

Este decorador no hace nada

hola

>>> saludo2.name

'inner'

>>> def deco(f):

... def inner(a, *kw):

... print "Este decorador no hace nada"

... return f(a, *kw)

... return functools.update_wrapper(inner, f)

...

>>> saludo3 = deco(saludo)

>>> saludo3()

Este decorador no hace nada

hola

>>> saludo3.name

'saludo'

>>> saludo = saludo3



8 de mayo: Python Day en Rafaela

El próximo sábado se va a desarrollar en la ciudad de Rafaela un Python Day, un día de charlas para que quienes no conozcan Python puedan acercarse al lenguaje. Vamos a tener muchas charlas introductorias y esperamos pueda aprovechar este evento tanto la comunidad universitaria como la ciudad en general.

http://www.pyday.com.ar/rafaela2010/

Por mi parte voy a estar colaborando con una charla nueva: Entendiendo Decoradores en Python. Esperemos salga bien :)

El Python Day es un evento organizado por PyAr con el apoyo de la Universidad Católica de Santiago del Estero, Departamento Académico Rafaela. El evento dura un día donde se darán pequeños cursos y charlas relacionadas a este lenguaje que de a poco va a haciendo su lugar entre los desarrolladores. Tenemos como intención hacer un espacio en donde cualquier persona interesada pueda acercarse para preguntar, aprender y experimentar con estas herramientas. El evento se llevará a cabo acá a partir de las 9:00 de la mañana. ¿Qué es Python? Python es un lenguaje de programación interpretado creado por Guido van Rossum en el año 1990. En la actualidad Python se desarrolla como un proyecto de código abierto, administrado por la Python Software Foundation. La última versión estable del lenguaje es la 2.6 (01 de octubre de 2008). Fuente: Wikipedia Quiero participar… ¿Qué Hago? Si lo que querés es asistir a las charlas, solo aparecete por la universidad el 8 de mayo a partir de las 09:00 y listo (por favor, si es posible, registrate previamente por web)


Aplicar un decorador a todas las funciones de un módulo en Python

En la lista de PyAr preguntaron si había alguna forma de aplicar un decorador a todos las funciones de un módulo. Envié una solución sin probarla, que al verla unos días más tarde parece bastante buena :)

La comento aquí con un ejemplo. modulo.py contiene definiciones de funciones:

def a():
pass

def b():

print 42

def c():

a()

b()</pre>

y decoradores.py un decorador que imprime el nombre de la función llamada:

def nombrador(f):

    def inner(*a, **kw):

        print "Ejecutando %s" % f.__name__

        return f(*a, **kw)

    return inner

(Si no sabés lo que es un decorador, podés leer mi post Decoradores en Python I: Introducción)

En lugar de modificar las definiciones de funciones en modulo.py para aplicar el decorador a cada una de las funciones, ya sea usando el azúcar sintáctica de Python:

@nombrador

def a():

    ...

o mediante una llamada a la función:

a = nombrador(a)

podemos agregar el siguiente código al final de modulo.py:

for n,v in locals().items():

   if inspect.isfunction(v) and n != 'nombrador':

       locals()[n] = nombrador(v)

Vamos a explicarlo:

la llamada a la función built-in locals retorna un diccionario representando el espacio de nombres local: cada clave es un string representando el nombre de un objeto y cada valor es el objeto en si. Iteramos sobre la lista de pares (key, value) del mencionado dict y por cada uno verificamos si:

a) es una función (inspect.isfunction es apropiado para esto)

b) el nombre no es el del decorador que queremos aplicar (para no aplicar el decorador sobre si mismo!)

Si las condiciones a y b se cumplen, podemos guardar en el diccionario del espacio de nombres, bajo el nombre de la función que cumplió las condiciones, una versión decorada de la misma.

Agregamos algo más de código a modulo.py para que se llame a las funciones cuando lo ejecutemos:

if __name__ == '__main__':

    a()

    b()

    c()

Esta es la salida obtenida:

juanjo@fenix:~/python/muchosdecos$ python modulo.py

Ejecutando a

Ejecutando b

42

Ejecutando c

Ejecutando a

Ejecutando b

42

¿Querés probarlo? Bajá muchos.zip

Nota: para acceder a locals() no se puede utilizar iteritems por que el diccionario cambia durante la ejecución.


Decoradores en Python (III) - Clases decoradoras

Siguiendo con la serie de posts sobre decoradores en Python, y fiel al espíritu que los originó (ir mostrando lo que voy aprendido a medida que necesito resolver problemas específicos o descubro aplicaciones concretas) hoy les traigo un nuevo uso para los decoradores en Python: funciones caché.

Anteriormente: Decoradores I, Decoradores II.

Funciones caché

Una función caché[0], es aquella que siempre que se le pide que compute un resultado para un grupo de parámetros dado, primero se fija en una memoria interna si no realizó ya el cálculo. Si ya lo hizo, retorna el valor computado anteriormente. Si aún no lo hizo, computa el valor, lo guarda en una memoria interna y luego lo retorna.

Esta técnica es muy útil en funciones que requieren un cómputo intensivo y obtener un resultado lleva mucho tiempo. Permita acelerar sustancialmente los tiempos de ejecución a cambio de utilizar más memoria.

La siguiente es una forma de implementarlo en Python para un computo en particular:

cache = {}

def fmem(arg):

    if arg in cache:

        print "Recuperando valor de la memoria"

        return cache[arg]

    else:

        r = (arg ** 10) * (arg ** -5)

        cache[arg] = r

        return r

Como memoria se utiliza un diccionario y el argumento de la función fmem es la clave del diccionario[1].

Este es el resultado de utilizarla en el intérprete interactivo:

>>> fmem(1)

1.0

>>> fmem(2)

32.0

>>> fmem(2)

Recuperando valor de la memoria

32.0

Decoradores con estado

En esta implementación, la técnica de memorización se mezcla con el cálculo que era el objetivo original de la función. Si queremos aplicar la técnica sobre distintas funciones vamos a tener que entrometer la implementación de la caché en todas las funciones. Peor aún, si en el futuro se quiere realizar un cambio en la forma de almacenar y recuperar los valores almacenados, ¡tendríamos que modificar todas las funciones! La forma de resolver estos problemas es implementando un decorador que agregue esta funcionalidad a las funciones decoradas: resolvemos ambos problemas, el de intrución y el de mantenibilidad. Todo el código que provee esta funcionalidad extra es encapsulado en el decorador.

Las funciones decoradoras, como las que vimos en los anteriores artículos, no nos sirven para esta tarea. Necesitamos un decorador que pueda almacenar un estado. Ya que cualquier callable puede ser un decorador, implementaremos el decorador mediante una clase.

Funciones caché con clases decoradoras

La definición de la clase decoradora consiste en dos métodos:

  • un método de inicialización, dónde se inicializa el atributo cache con un diccionario vacío y se guarda una referencia a la función decorada.
  • un método __call__ que será ejecutado cuando se llame a la función decorada.
class mem(object):



    def __init__(self, g):

        self.cache = {}

        self.g = g



    def __call__(self, arg):

        if arg in self.cache:

            print "Recuperando valor de la memoria"

            return self.cache[arg]

        else:

            r = self.g(arg)

            self.cache[arg] = r

            return r

Luego, lo único que resta es decorar todas las funciones que querramos "dotar de memoria" para obtener mejoras de performance en su ejecución:

@mem

def f(arg):

    return (arg ** 10) * (arg ** -5)

La salida obtenida al ejecutar la función decorada en el intérprete interactivo es la misma qué en el ejemplo anterior:

>>> fmem(1)

1.0

>>> fmem(2)

32.0

>>> fmem(2)

Recuperando valor de la memoria

32.0

Más

La implementación del decorador mem solo sirve para decorar funciones que reciben un único argumento. Podemos mejorar su definición para que pueda decorar funciones con cualquier número de argumentos:

class mem2(object):



    def __init__(self, g):

        self.cache = {}

        self.g = g



    def __call__(self, *args):

        if args in self.cache:

            print "Recuperando valor de la memoria"

            return self.cache[args]

        else:

            r = self.g(*args)

            self.cache[args] = r

            return r
@mem2

def f2(arg1, arg2):

    return (arg1 ** 10) * (arg2 ** -5)

Notas

[0] Se puede leer más sobre este concepto en Caching Function Results:Faster Arithmetic by Avoiding Unnecessary Computation de Stephen E. Richardson [SMLI TR-92-1]

[1] Esta implementación tiene la limitación de que si el argumento de la función es un objeto mutable, no podrá ser usado como clave de un diccionario y se lanzará una excepción.


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

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á.


Decoradores en Python (I) - Introducción

Este artículo es el primero de un plan de 3 artículos. Empezamos con una introducción a los decoradores en Python.

Funciones

Cómo todo en Python, las funciones son objetos. La forma más común de crear un objeto de tipo <function> es mediante el keyword def:

def saludo():

    print "Hola"

Al realizar esta definición, el cuerpo de la función es compilado pero no ejecutado y el objeto de tipo <function> es asociado al nombre 'saludo'. Mediante este nombre podemos referirnos al objeto:

>>> saludo

<function saludo at 0xb7d82fb4>

y utilizando la notación de paréntesis podemos llamar a (ejecutar) la función.

>>> saludo()

Hola

Una función puede tener parámetros (los parámetros son nombres a los que podemos referirnos en el cuerpo de la función):

def saludo2(nombre):

    print "Hola %s" % nombre
def saludo3(nombre, apellido):

    print "Hola %s %s" % (nombre, apellido)

Cuando llamamos a la función con argumentos (los argumentos son valores que en principio se asocian uno a uno a los parámetros de la función):

>>> saludo2("Ceci")

Hola Ceci
>>> saludo3("Ceci", "Pucci")

Hola Ceci Pucci

Los últimos n parámetros pueden tener valores por defecto, entonces estas definiciones y sus consiguientes ejecuciones son válidas:

def saludo4(nombre, apellido="Conti"):

    print "Hola %s %s" % (nombre, apellido)
>>> saludo4("Juanjo")

Hola Juanjo Conti
>>> saludo4("Juanjo", "Garau")

Hola Juanjo Garau
def saludo5(nombre="Juanjo", apellido="Conti"):

    print "Hola %s %s" % (nombre, apellido)
>>> saludo5()

Hola Juanjo Conti
>>> saludo5("Mary")

Hola Mary Conti

Los últimos n argumentos pueden ser argumentos nombrados, es decir utilizando el nombre de los parámetros con los que el argumento se debe asociar. En las siguientes ejecuciones se pueden ver ejemplos de esto:

def saludo6(tratamiento, nombre, apellido):

    print "Hola %s %s %s" % (tratamiento, nombre, apellido)
>>> saludo6("Sr.", apellido="Conti", nombre="Juanjo")

Hola Sr. Juanjo Conti
>>> saludo6("Sr.", "Juanjo", apellido="Conti")

Hola Sr. Juanjo Conti

Los parámetros de una función pueden terminar con <nombre> (una tupla con los últimos argumentos posicionales) y/o *<nombre> (un diccionario con los últimos argumentos nombrados).

def saludo7(tratamiento, *args):

    print "Hola %s %s" % (tratamiento, " ".join(args))
>>> saludo7("Sr.", "Juanjo", "Conti")

Hola Sr. Juanjo Conti
>>> saludo7("Sr.", "Juanjo", "Conti", "Garau")

Hola Sr. Juanjo Conti Garau

Notemos que esta forma de definir una función es bastante útil cuando no sabemos el número de argumentos que se recibirán.

La siguiente es la forma más genérica de definir una función:

def saludo8(*args, **kwargs):

    pass

Decoradores

Un decorador es una función 'd' que recibe como argumento otra función 'a' y retorna una nueva función 'b'. La nueva función 'b' es la función 'a' decorada con 'd'.

Supongamos que queremos avisarle a un sistema de seguridad cada vez que se ejecutan las funciones abrir_puerta y cerrar_puerta. Para hacer una simplificación, el aviso simplemente será imprimir por un mensaje en la pantalla. Podemos escribir el siguiente 'decorador':

def avisar(f):

    def inner(*args, **kwargs):

        f(*args, **kwargs)

        print "Se ha ejecutado %s" % f.__name__

    return inner

Las siguientes son las funciones a decorar:

def abrir_puerta():

    print "Abrir puerta"



def cerrar_puerta():

    print "Cerrar puerta"
>>> abrir_puerta()

Abrir puerta

>>> cerrar_puerta()

Cerrar puerta

Y ahora solo nos limitamos a seguir la definción que di al principio de un decorador:

abrir_puerta = avisar(abrir_puerta)

cerrar_puerta = avisar(cerrar_puerta)

Listo!, ambas funciones han sido decoradas:

>>> abrir_puerta()

Abrir puerta

Se ha ejecutado abrir_puerta

>>> cerrar_puerta()

Cerrar puerta

Se ha ejecutado cerrar_puerta

Azúca sintáctica

En Python 2.3, la anterior era la forma de decorar una función. A partir de Python 2.4 se a añadido azúcar sintáctica al lenguaje que nos permite hacer lo mismo de esta forma:

@avisar

def abrir_puerta():

    print "Abrir puerta"



@avisar

def cerrar_puerta():

    print "Cerrar puerta"

Esta es una forma mucho más visual de hacerlo.

Encadenando decoradores

La decoración de funciones puede encadenarse. Para ejemplificarlo vamos a suponer ahora que solo usuarios autenticados en el sistema pueden ejecutar las funciones abrir_puerta y cerrar puerta.

Nuevamente hacemos una simplificación. Existe la variable AUTHENTICATED que indica el estado del usuario actual. Si el usuario no está autenticado y se intente ejecutar alguna de las funciones, una excepción es lanzada.

def autenticado(f):

    def inner(*args, **kwargs):

        if AUTHENTICATED:

            f(*args, **kwargs)

       else:

           raise Exception

    return inner

Luego, la definición de abrir_puerta y cerrar_puerta debería ser:

@autenticado

@avisar

def abrir_puerta():

    print "Abrir puerta"



@autenticado

@avisar

def cerrar_puerta():

    print "Cerrar puerta"

Con AUTHENTICATED = True:

>>> cerrar_puerta()

Cerrar puerta

Se ha ejecutado cerrar_puerta

Pero si AUTHENTICATED = False:

>>> cerrar_puerta()

Traceback (most recent call last):

File "<stdin> ", line 1, in <module>

File "<stdin> ", line 6, in inner

Exception

update: 2° entrega.