jueves, 13 de marzo de 2014

Python exquisite III, decoradores



Esta entrada voy a dedicarla a algunos conceptos importantes, en muchos sentidos básicos, pero que son necesarios para entender el uso de los decoradores en Python. Más del 60% del contenido lo he sacado del blog de Simeon Flanklin, por lo que recomiendo una visita al mencionado blog.

Empecemos por conceptos sencillos que resultan imprescindibles para entender el funcionamiento de los decoradores:

  • Las funciones crean un nuevo ámbito, o un nuevo espacio de nombres para las variables. Esto es; las funciones tienen su propio espacio de nombres.
var_global = 10
>>> def fun():
...     var_local = -10
...     var_global = -1
...     print var_global
...     print locals()
... 
>>> fun()
-1
{'var_local': -10, 'var_global': -1}
>>> var_global
10
>>> globals()
{'__builtins__': , 'var_global': 10, 
'__package__': None, 'fun': , '__name__': '__main__', '__doc__': None}


Como puede verse las variables globales no se modifican dentro de una función, se puede trabajar con ellas pero realmente se trabajaría con una copia local. De igual manera las variables locales solo son visibles desde la función que las crea.
  • Python permite funciones dentro de funciones, es decir, se permiten las funciones anidadas. 

x_global = -10
def fun_out():
    x = 10 + x_global
    print locals()
    def fun_inn():
        print locals()
        x = x_global + 1 
        print locals()
    fun_inn()
    print locals()
fun_out() 

Al ejecutar lo anterior tenemos:



x_global es una variable global por lo que no puede modificarse dentro de las funciones, por otro lado x es una variable local para la función fun_out , al igual que la función interna fun_inn. Un detalle importante es que fun_inn no puede ver la variable local x de fun_out, es decir, las variables locales de fun_out no pasan a ser globales para las funciones internas. 

  • En Python las funciones admiten funciones como argumentos, también pueden devolver funciones. 
 
def operation(func,x,y):
    return func(x,y)
def mult(x,y):
    return x*y
def div(x,y):
    return x/y if y != 0 else "Division por cero"

print operation(div,12,0)

La salida es:



  • Closures: En principio el siguiente código no debería funcionar:
def outer(x):
    def inner():
        print x
    return inner

print0 = outer(0)
print1 = outer(1)
print print0.func_closure
print0()
print1()
>> 0
>> 1 

Y no debería funcionar ya que x es una variable local de la función outer y como ouder devuelve la función inner, no se ejecuta inner hasta que outer ha finalizado. x solo existe mientras dura outer, por tanto cuando inner hace uso de x, esta ya no debería existir al haber desaparecido junto con outer. Sin embargo Python cuenta con las Closures  de una función, que es la capacidad que tiene inner para recordar estas variables locales aunque su ámbito este destruido.

  •  Decoradores: Fijémonos en el siguiente código:

class Vector(object):
    def __init__(self,vx,vy,vz):
        self.x = vx
        self.y = vy        
        self.z = vz
    def __repr__(self):
        return " Coordinates " + str(self.__dict__)

def prod(v,w):
    return Vector(v.y*w.z - v.z*w.y,v.z*w.x - v.x*w.z, v.x*w.y - v.y*w.x)

v = Vector(2,0,1)
w = Vector(1,-1,3)
v = prod(v,w)  


La función producto realiza el producto vectorial, pero nos dará un error si intentamos realizar el producto entre un escalar y un vector. Si quisiéramos incorporar el producto entre un escalar y un vector podríamos añadir una nueva función, complicar algo más el código de la función producto o realizar una función DECORADORA  que modifique prod añadiéndole nueva funcionalidad, es decir, "decorando" nuestra función producto. Usando una función decoradora tendríamos:

class Vector(object):
    def __init__(self,vx,vy,vz):
        self.x = vx
        self.y = vy        
        self.z = vz
    def __repr__(self):
        return " Coordinates " + str(self.__dict__)

def prod(v,w):
    return Vector(v.y*w.z - v.z*w.y,v.z*w.x - v.x*w.z, v.x*w.y - v.y*w.x)

def cover(fun):
    def check(v,w):
        if type(v) == type(1) or type(v) == type(1.0):
            ret = Vector(v*w.x,v*w.y,v*w.z)
        else:
            ret = fun(v,w)
        return ret
    return check

prod = cover(prod)
v = Vector(2,0,1)
w = Vector(1,-1,3)
q = prod(v,w)
r = prod(2,w)
print q
print r 

La salida funciona como esperabamos:



La función cover es precisamente un decorador y modifica o decora a cualquier función (que tenga los mismos argumentos que check).

EL símbolo "@" puede ser usado para decorar una función desde su implementación. El anterior ejemplo podría sustituirse por el siguiente código:

class Vector(object):
    def __init__(self,vx,vy,vz):
        self.x = vx
        self.y = vy        
        self.z = vz
    def __repr__(self):
        return " Coordinates " + str(self.__dict__)


def cover(fun):
    def check(v,w):
        if type(v) == type(1) or type(v) == type(1.0):
            ret = Vector(v*w.x,v*w.y,v*w.z)
        else:
            ret = fun(v,w)
        return ret
    return check
    
@cover
def prod(v,w):
    return Vector(v.y*w.z - v.z*w.y,v.z*w.x - v.x*w.z, v.x*w.y - v.y*w.x)    

v = Vector(2,0,1)
w = Vector(1,-1,3)
q = prod(v,w)
r = prod(2,w)

De esta forma prod  se decora mediante cover. 

  • *args y **kwargs: Un problema evidente de los decoradores es que solo decoran funciones con un número de argumentos determinado. Por ejemplo, cover solo puede decorar funciones con dos argumentos, por lo que es difícil generalizarlo a otros casos. Esto tiene una solución, pero antes vamos a explicar *args y **kwargs. Con *args accedemos a una lista arbitraría de argumentos, el siguiente ejemplo es clarificador:
>>> def add_one(*args):
>>>     return list(x+1 for x in args)
>>> print add_one(1,2,3,4)
>>> (2,3,4,5)


Es decir, con *args empaquetamos los argumentos en una tupla y ya no hay que preocuparse de la cantidad de argumentos. **kwargs funciona de forma parecida pero trabajando con diccionarios, un ejemplo lo deja claro:

>>> def foo(**kwargs):
>>>    print kwargs
>>> print foo(a=1,b=2)
>>> {'y': 2, 'x': 1}
 
 También pueden usarse clases como decoradores, el siguiente ejemplo ilustra como podemos añadir un pequeño decorador que ofrece logging para cada función que se quiera decorar:

class LogFunc(object):
    def __init__(self,func):
        self.func = func
    def __call__(self,*args):
        print "[LOGGING] Entrando en la funcion: ", self.func.__name__
        print "[LOGGING] Con argumentos ", args        
        ret = self.func(*args)
        print "[LOGGING] Saliendo de ", self.func.__name__, " y ofreciendo el resultado" 
        return ret
        

@LogFunc 
def function1(x,y):
    return x+y
@LogFunc
def function2(x,y,z):
    return x+y++z
    
print function1(1,2)
print function2(1,2,5)

La salida es la siguiente:



 Para ver más ejemplos útiles de decoradores se puede consultar aquí.

No hay comentarios:

Publicar un comentario