El diablo está en los detalles, Python edition (y II)

En mi anterior post (y con “anterior” quiero decir “de hace más de un mes”, la frecuencia de mis entradas va a quedarse lejos de ser legendaria…) estuve comentando algunas características de Python que me parecían especialmente útiles. Herramientas como las comprensiones de listas y los generadores multiplican la productividad y hacen que el lenguaje sea compacto y legible. Un 2×1, como en los anuncios de detergente. Pero se me fue un poco de largo, y dejé fuera del mismo varias características igual de interesantes que quería explicar también. Si os parece bien, hoy echaremos un ojo a alguna más. Al final sólo a una más: ¡los decoradores!

Decoradores

Si vienes de Lisp y amigos y te hablo de metaprogramación en Python posiblemente te rías un rato, y tendrás razón. Python (o Ruby, o cualquiera de los demás lenguajes dinámicos que alardean de su capacidad de introspección) palidecen frente a las macros de Lisp en todo lo tocante a la metaprogramación, esto es así. Si el palabro os suena un poco raro, el concepto es sencillo: la metaprogramación es la manipulación y modificación de código por otro código. Literalmente, es un programa programando, un programa que se modifica a sí mismo u a otros programas, vamos.

En Lisp es un concepto muy natural: el código Lisp es una estructura de datos de Lisp válida (expresiones S, para los curiosos). Por tanto un programa (o fracción del mismo) en Lisp es un valor de entrada válido para una función en Lisp, y puede ser manejado como una estructura de datos más. Es apasionante, pero no es el caso de Python, claro, donde el código no se expresa como una estructura de datos del lenguaje. Aún así Python ofrece cierto grado de metaprogramación. Ciertas construcciones del lenguaje permiten modificar porciones de código existente, y de todas ellas los decoradores son sin duda las más amigables y usadas.

Los decoradores son funciones que modifican funciones (en realidad también pueden decorarse clases, pero centrémonos en las funciones por ahora). De hecho un decorador no es más que azúcar sintáctico para pasar una función como parámetro a otra función y obtener una tercera función como resultado. Toma ya, y ahora a ver cómo aclaro esto un poco (para la gente de mi edad que veía Super3 hace años, “la força del superguerrer que ha superat la força del superguerrer que…”). Creo que la mejor manera de explicar un decorador es ilustrándolo con un ejemplo, y de golpe todo tendrá mucho más sentido. Veamos primero que pinta tiene, y cual es el su función. Luego entramos en harina y miramos las tripas.

Un ejemplo muy ilustrativo son los decoradores de autorización de Django. Cuando se desarrolla una aplicación web, es bastante estándar el quere proteger ciertas características con autenticación. O sea, o el usuario está validado en el sistema y dispone de ciertos permisos, o no puede acceder a la característica. Django ofrece mecanismos para hacer esas comprobaciones de manera genérica y reusable. Para mi ejemplo voy a implementar un decorador (muy chorra él) que emule ese funcionamiento. Si el usuario no está validado (en nuestro ejemplo el usuario es nulo) no queremos que se ejecute nuestra función, sino darle un mensaje de error al usuario. Al turrón:

@is_authenticated    # esta es una función ya definida, luego la vemos
def my_secret_feature(user):
    print 'pasa pa dentro %s' % user

# usuario autenticado, debería poder entrar
user = 'aitor'
my_secret_feature(user)
# resultado -> pasa pa dentro aitor

# usuario no autenticado, debería recibir error
user = None
my_secret_feature(user)
# resultado -> sin pendiente no entras

Bien, el tema es el siguiente. Mi función my_secret_feature recibe un usuario como parámetro. Si el usuario es cualquier cosa menos nulo (None en Python), la función le da la bienvenida. Si el usuario es nulo, se pinta un mensaje de error (¡un rechazo en toda regla!) y no se ejecuta el cuerpo de la función. ¿Cómo es eso posible si la función no hace ninguna comprobación? Pues la magia está en ese @is_authenticated que precede a la definición de my_secret_feature. Ese es el decorador.

La sintaxis @ + nombre de función crea un decorador, y al aplicarse a una función (simplemente precediendo a su definición) lo que hace el intérprete es “envolver” la función con la función decoradora. Ese envolvimiento consiste en pasar a la función decoradora la función decorada como parámetro, y la primera ha de devolver una nueva función, que es la que realmente estamos invocando. Esa nueva función puede hacer lo que le plazca, teniendo acceso a la función original y sus parámetros. De manera más visual, estas dos sintaxis son equivalentes:

@is_authenticated
def my_function(arg):
    # cuerpo de la función

my_function('foo')

 

def my_function(arg):
    # cuerpo de la función

new_func = is_authenticated(my_function)
new_func('foo')

¿Se entiende el asunto? Lo que hace el @ es llamar a is_authenticated con my_function como parámetro, y retornar una nueva función (en la versión explícita new_func).  A partir de ese momento, cualquier invocación de my_function es en realidad una invocación de new_func. El decorador hace todo ese proceso de funciones que toman funciones como parámatros y devuelven funciones invisible, facilitando mucho el uso (y sobretodo la reutilización) de esos modificadores. ¿Y que pinta tiene una función decoradora? Pues echemos un ojo a la definición de is_authenticated:

def is_authenticated(func):
    def new_func(user):
        if user is None:
            print 'sin pendiente no entras'
            return
        return func(user)
    return new_func

Explicado rápidamente:

  • is_authenticated toma una función como parámetro
  • define una nueva función (en Python pueden definirse funciones anidadas, vamos, dentro de otras funciones) new_func que acepta un parámetro user
  • la lógica de new_func es:
    • si el parámetro user es nulo, se imprime el mensaje y se sale sin hacer nada más
    • en caso contrario, se llama a la función que is_authenticated ha recibido como parámetro pasando el parámetro user a la misma
  • is_authenticated retorna new_func, que a partir de entonces será la que invoquemos realmente

Volviendo a nuestro ejemplo inicial, lo que está pasando es que is_authenticated nos retorna new_func, aunque para nosotros se sigue llamando my_secret_feature. Al invocarla pasando user como parámetro, new_func comprueba si user es nulo. De serlo, pinta el error y retorna, jamás se invoca my_secret_feature original. De no serlo, se invoca my_secret_feature (es la func que ha recibido is_authenticated durante la decoración) pasándole el mismo user que hemos pasado a new_func. Y esa es la magia.

En fin, esto es sólo una parte de la potencia de los decoradores. Se pueden implementar decoradores como clases y no como funciones, lo que permite organizar mejor el código de decoradores complejos. Los decoradores pueden recibir sus propios parámetros, además de los de la función decorada, haciéndolos muy flexibles (un ejemplo que me gusta mucho es el decorador de caching de Django, dónde explicitas por cuántos segundos es válida la caché con @cache(num_segundos)). Y las clases también pueden ser decoradas (en vez de modificar una función, modificamos una clase). No es metaprogramación al nivel de Lisp, pero cambiar el comportamiento por defecto de ciertas porciones de código es muy fácil y elegante gracias a los decoradores.

Volvía a haber más, pero…

Soy un planificador de posts horrendo. Tenía intención de tocar al menos dos temas, pero me he liado con los decoradores y me he ido a las 1200 palabras en un santiamén. Como la idea es no aburrir (y seguir teniendo tema para escribir más posts, que tengo pocas ideas) vamos a dejarlo por hoy. Si tenéis dudas, aportaciones, correcciones, ahí están los comentarios. Si queréis que siga la “saga”, insistid también, así me obligo a darle un rato a la tecla. Y si queréis proponer tema para otro post, yo encantado, nunca sé sobre qué charlar un rato.

Muchas gracias por leer, como siempre. Y echadle un ojo Lisp, venga ;)

Advertisements

14 thoughts on “El diablo está en los detalles, Python edition (y II)

  1. Sí, que siga la saga por favor :D

    ¡Muy bien explicados los decoradores! No son un tema sencillo para la gente que está empezando. Y lo de echarle un ojo a Lisp… bueno, si insistes ;)

    1. Había pensando en hablar de functools.wraps, hacer algún ejemplo con clases, enseñar como pasar parámetros… pero me ha parecido que todo eso era hacer un manual de decoradores, y la idea es sólo introducir el concepto.

      Creo que los comentarios son un buen lugar para concretar, así que gracias por compartir el enlace :)

  2. Sí, por favor, sigue publicando… de verdad que, al menos a mí, me sirven para aclararme muchas cosas del funcionamiento de python.

    Gracias.

    1. No estoy muy puesto en los nuevos añadidos a PHP, pero hasta donde entiendo sus traits son mixins con cierto nivel de control de conflictos. Vamos, los traits añaden métodos a una clase, como en la herencia pero sin herencia.

      Los decoradores pueden modificar clases no sólo añadiendo métodos, sino quitándolos, añadiendo atributos, modificando sus valores por defecto, … y también pueden modificar funciones (los traits sólo aumentan clases, no funciones).

      1. Pues parecen una maravilla los decoradores estos. ¿qué tal se llevan con el lado ingenieril? Me refiero a que se debe ir con cuidado en qué se machaca, evitar duplicidades, etc, SOLID, vamos. (jaja, el lado SOLID de la vida)

        1. Bueno, los decoradores son una idea que bebe más bien de la tradición funcional, así que no sé hasta que punto respetan o incumplen los mandatos del buen diseño orientado a objetos :)

          1. Cristian, no deja anidar más, respondo aquí.

            Su uso es muy común en Python. De hecho, como comento en el artículo, los principales frameworks ofrecen sus propios decoradores. Permite separar la lógica de tu código de consideraciones como la autenticación, cacheado, etc…

            La librería estándar también provee un buen número de decoradores (marcar métodos como estáticos en clases, definir setters/getters de atributos, …) así que sí, los uso con frecuencia.

            Python incentiva su uso, así que me porto bien en cualquier caso :)

  3. La verdad es que me ha ayudado bastante este post a acabar de comprender los decoradores que hasta ahora no tenía nada claros, gracias!

    1. Los bloques son, muy probablemente, la característica de Ruby que más se “envidia” en Python.

      No dejan de ser funciones anónimas, que existen en Python (lambdas), pero con una gran limitación: permiten una única expresión. ¿La razón? Pues habría que preguntarle a Guido von Rossum, pero tiene muy claro que no piensa cambiarlo.

      Su argumento viene a ser que una función anónima no puede hacer nada que no pueda hacer una función normal, y que por tanto no vale la pena complicar el lenguaje.

      A mí, personalmente, me gustan los bloques de Ruby.

  4. Me encantan estos post en los que explicas conceptos avanzados de programación dentro de Python. He decidido “especializarme” en este lenguaje y me resultan de gran utilidad.

    Veo que ya ha pasado un tiempo y no te has animado a escribir la parte 3. Así que no puedo hacer otra cosa que unirme a la causa e insistirte un poquito más. Gracias por compartir tus conocimientos.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s