Decoradores en Python

Publicado por Andrea Navarro en

En este artículo se hará una introducción a decoradores en Python partiendo de un repaso de las características de las funciones que se utilizan como principios de los mismos.

Repaso de funciones

Nombres de funciones

El primer aspecto a recordar es que los nombres de las funciones hacen referencia a dichas funciones por lo que es posible tener más de un nombre haciendo referencia a la misma función.

En el siguiente ejemplo se define una función con un nombre, luego se iguala el valor de esa función a un nuevo nombre. De esta manera la función puede ser llamada utilizando cualquiera de los dos nombres.

#Define la función
def saludar():
	print("Hola!")

#Iguala el valor del nombre de la función a una nueva variable
saludo = saludar

#Llama a la función por el nombre con la que fue definida
saludar()
#Llama a la función por el nombre de la nueva variable
saludo()

Resultado:

Hola!
Hola!

Funciones dentro de funciones

A diferencia de otros lenguajes en Python es posible definir funciones dentro de otras funciones. Esto permite proteger la implementación de la función interior de el código externo permitiendo solo su acceso a través de la función que la contiene.

#Definir función
def saludar(nombre):
	#Definir función interna
    def saludo(nombre):
		#Implementación de la función interna 'saludo'
        return "Hola "+nombre+" "
    #Implementación de la función saludar
    print(saludo(nombre)+"encantado de conocerte")

saludar("María")
#saludo("María") #Esta linea genera error ya que no se puede acceder a la función saludo desde afuera de la función saludar

Resultado:

Hola María encantado de conocerte

El orden de ejecución en este ejemplo es el siguiente:

  1. Se llama a la función saludar con el parámetro «María«
  2. La función saludar ejecuta su implementación
  3. La linea de código que contiene el print llama a la función saludo que se encuentra definida dentro de la misma función y le pasa el parámetro «María«
  4. La función saludo ejecuta su implementación
  5. La función saludo devuelve el valor y termina
  6. La función saludar concatena el resultado de la función saludo con el resto del texto e imprime la cadena resultante

Si utilizando el mismo código se intentara llamar a la función saludo desde afuera de la función daría el siguiente error ya que solo puede ser accedida desde dentro de la función que la define.

 saludo("María")
NameError: name 'saludo' is not defined

Funciones como parámetros de funciones

Como se explicó anteriormente los nombres de las funciones hacen referencias a las mismas, por lo tanto es posible pasarlas como parámetros a otras funciones para ejecutarlas dentro de las mismas.

#Definir la función A con nombre de función pasada por parámetro
def funcionA(funcionParametro):
    print("Ejecutando función A")
    print("Llamando a función pasada por parámetro")
    #Llamar a la función por el nombre pasado por parámetro
    funcionParametro()

#Definir la función B
def funcionB():
   print("Ejecutando función B")

#Llamar a la función A pasando por parámetro el nombre de la función B          
funcionA(funcionB)

Resultado:

Ejecutando función A
Llamando a función pasada por parámetro
Ejecutando función B

El orden de ejecución de este ejemplo es el siguiente:

  1. Se llama a la función A pasando por parámetro el nombre que hace referencia a la función B
  2. Se ejecuta la implementación de la función A
  3. Se llama a la función utilizando el nombre que ha sido pasado por parámetro (función B)
  4. Se ejecuta la implementación de la función B

Si el nombre de la función fuera el de otra variable que haya sido igualada al nombre de la función B está se ejecutaría igualmente ya que ambas variables harían referencia a esta función como se ve en el siguiente ejemplo.

#Definir la función A con nombre de función pasada por parámetro
def funcionA(funcionParametro):
    print("Ejecutando función A")
    print("Llamando a función pasada por parámetro")
    #Llamar a la función por el nombre pasado por parámetro
    funcionParametro()

#Definir la función B
def funcionB():
   print("Ejecutando función B")
#Igualar una nueva variable al nombre de la función B
fb = funcionB
#Llamar a la función A pasando por parámetro el nuevo nombre referenciando a la función B          
funcionA(fb)

El nombre original de la función (aquel con el que fue definido) puede obtenerse mediante el atributo __name__

#Definir la función A con nombre de función pasada por parámetro
def funcionA(funcionParametro):
    print("Ejecutando función A")
    #Imprimir el nombre de la función pasada por parámetro
    print("Llamando a función pasada por parámetro "+ funcionParametro.__name__)
    #Llamar a la función por el nombre pasado por parámetro
    funcionParametro()

#Definir la función B
def funcionB():
   print("Ejecutando función B")

fb = funcionB
#Llamar a la función A pasando por parámetro el nombre de la función B          
funcionA(fb)

Resultado:

Ejecutando función A
Llamando a función pasada por parámetro funcionB
Ejecutando función B

Funciones como retorno de funciones

De la misma manera que las funciones pueden ser pasadas por parámetro también pueden ser devueltas como retorno de una función.

#Definir la función saludar que toma como parámetro un nombre 
def saludar(nombre):
	#Definir función interna que toma como parámetro un valor bool
	def saludo(dia):
		if(dia):
			mensaje = "Buenos días "
		else:
			mensaje = "Buenas noches "
		return  mensaje+ nombre+"!!"
	#Devolver función interna
	return saludo

#Igualar la variable saludoPedro a la función 'saludo' que retorna la función 'saludar'	con el parámetro nombre igualado a Pedro
saludoPedro = saludar("Pedro")

#Ejecutar la función asignada pasando por parámetro el valor de 'día' como verdadero
print(saludoPedro(True))

Resultado

Buenos días Pedro!!

El orden de ejecución de este ejemplo es el siguiente:

  1. Se llama a la función saludar pasándole el parámetro nombre con el valor «Pedro»
  2. Se ejecuta la implementación de la referencia a la función interna saludo con el parámetro nombre ya seteado al valor «Pedro«
  3. Se iguala la variable saludoPedro a la referencia retornada por la función
  4. Se llama a la función saludoPedro que hace referencia a la función interna saludo pasándole el parámetro día con el valor True
  5. Se ejecuta la implementación de saludo
  6. La función concatena el valor dia pasado por parámetro con el valor nombre ya pasado por la función principal saludar
  7. La función devuelve el mensaje concatenado
  8. Se imprime el mensaje

Se puede ver la funcionalidad del código de manera más descriptiva en el siguiente ejemplo donde se llama a la función saludar con dos parámetros distintos: «Pedro» y «María», cada uno de esos retornos son guardados en las variables «saludoPedro» y «saludoMaria» respectivamente. Luego ambas funciones son llamadas utilizando los parámetros True y False.

#Definir la función saludar que toma como parámetro un nombre 
def saludar(nombre):
	#Definir función interna que toma como parámetro un valor bool
	def saludo(dia):
		if(dia):
			mensaje = "Buenos días "
		else:
			mensaje = "Buenas noches "
		return  mensaje+ nombre+"!!"
	#Devolver función interna
	return saludo

#Igualar la variable saludoPedro a la función 'saludo' que retorna la función 'saludar'	con el parámetro nombre igualado a Pedro
saludoPedro = saludar("Pedro")
#Igualar la variable saludoMaría a la función 'saludo' que retorna la función 'saludar'	con el parámetro nombre igualado a María
saludoMaria = saludar("María")

#Ejecutar la función asignada pasando por parámetro el valor de 'día'
print(saludoPedro(True))
print(saludoPedro(False))
print(saludoMaria(True))
print(saludoMaria(False))

Resultado:

Buenos días Pedro
Buenas noches Pedro
Buenos días María
Buenas noches María

Decoradores

El concepto del decorador se extiende de las funcionalidad de las funciones de Python ya vistas. Su función es la de «decorar» el comportamiento de una función de manera que sea posible agregar funcionalidad antes y/o después de su ejecución, condicionar su ejecución a otros valores, etc.

Ejemplo de decoración

Antes de ver un ejemplo de decorador utilizando la sintaxis propia de Python veremos un ejemplo de decoración. En este ejemplo tenemos una función sencilla llamada presentarse, esta toma un parámetro nombre e imprime un mensaje.

def presentarse(nombre):
    print("Mi nombre es "+nombre)

Supongamos que queremos agregar cierto comportamiento para que se realice antes y después de la ejecución de esta función, en este caso que se impriman dos mensajes más. Para esto vamos a crear una función decoradora llamada saludar, esta tendrá a su vez una función interna llamada saludo que se encargará de ejecutar la función original presentarse más los mensajes extra.

Lo que se intenta lograr con el código es «decorar» la función original presentarse para que al ejecutarla en realidad se ejecute la función interna saludo que ejecutará la versión decorada. A esta función interna del decorador la denominaremos wrapper.

#Definir función del decorador que recibe el nombre de la función a ejecutar
def saludar(funcion):	
	#Definir el wrapper que recibe como parámetro el pasado por la función decorada 
    def saludo(parametro):
        print("Buenos días")
        #Llamar a la función decorada
        funcion(parametro)
        print("Hasta luego!")
    #Devolver el wrapper
    return saludo

#Definir función decorada
def presentarse(nombre):
    print("Mi nombre es "+nombre)
    
#Llamar función sin decorar
presentarse("María")   

#Decorar función 
presentarseDecorada = saludar(presentarse)
#Llamar función decorada
presentarseDecorada("María")

Resultado:

#Resultado de la función sin decorar
Mi nombre es María 
#Resultado de la función decorada
Buenos días
Mi nombre es María
Hasta luego!

En el caso de la decoración de la función el orden de ejecución es el siguiente:

  1. Se llama a la función decoradora saludar y se le pasa por parámetro la función a decorar (presentarse)
  2. La función saludar devuelve el wrapper saludo con el parámetro función cargado con el valor presentarse
  3. El wrapper retornado es guardado en la variable presentarseDecorada
  4. Se llama a presentarseDecorada que hace referencia al wrapper pasándole como parámetro el valor «María«
  5. Se ejecuta el wrapper saludo
  6. saludo imprime el primer mensaje
  7. saludo llama a la función presentarse
  8. Se ejecuta la función presentarse
  9. La función presentarse imprime el mensaje
  10. saludo ejecuta el segundo mensaje

Sintáxis de decorador

El ejemplo anterior puede lograrse con un código más reducido utilizando la sintáxis de Python para decoradores.

En el ejemplo era necesario llamar a la función decoradora y almacenar el resultado en una variable que luego debía ejecutarse para obtener el comportamiento de la función decorada. Utilizando la sintaxis se puede lograr el mismo efecto colocando con un @ el nombre de la función decoradora que se aplicará, una vez realizado esto cualquier llamada a la función dará como resultado la implementación de la función decorada.

Si convertimos el ejemplo anterior:


#Definir función del decorador que recibe el nombre de la función a ejecutar
def saludar(funcion):	
	#Definir el wrapper que recibe como parámetro el pasado por la función decorada 
    def saludo(parametro):
        print("Buenos días")
        #Llamar a la función decorada
        funcion(parametro)
        print("Hasta luego!")
    #Devolver el wrapper
    return saludo

#Definir función decoradora
@saludar
#Definir función decorada
def presentarse(nombre):
    print("Mi nombre es "+nombre)
    
#Llamar función decorada
presentarse("María")

Resultado:

Buenos días
Mi nombre es María
Hasta luego!

Ahora es posible crear una nueva función y utilizar el mismo decorador saludar siempre y cuando la nueva función tenga un solo parámetro al igual que presentarse.

#Definir función del decorador que recibe el nombre de la función a ejecutar
def saludar(funcion):	
	#Definir el wrapper que recibe como parámetro el pasado por la función decorada 
    def saludo(parametro):
        print("Buenos días")
        #Llamar a la función decorada
        funcion(parametro)
        print("Hasta luego!")
    #Devolver el wrapper
    return saludo

#Definir función decoradora
@saludar
#Definir función decorada
def presentarse(nombre):
    print("Mi nombre es "+nombre)
    
#Definir función decoradora
@saludar
#Definir función decorada
def contarHistoria(titulo):
    print("Quiero contarte una historia titulada '"+titulo+"'....")
    
#Llamar función decorada
presentarse("María")
contarHistoria("La isla del tesoro")

Resultado:

#Presentarse
Buenos días
Mi nombre es María
Hasta luego!
#ContarHistoria
Buenos días
Quiero contarte una historia titulada 'La isla del tesoro'....
Hasta luego!

Parámetros del decorador

Como veíamos en el ejemplo anterior solo podíamos reutilizar el decorador para funciones con la misma cantidad de parámetros. Para sortear este problema se debe modificar el wrapper de manera que acepte una cantidad indeterminada de parámetros utilizando las sintaxis *args y **kwargs.

En el siguiente ejemplo se decoran tren funciones diferentes, la función presentarse a la que se le pasa un solo parámetro, la función contarHistoria que tiene dos y la función mostarEstado que no toma ningún parámetro.

#Definir función del decorador que recibe el nombre de la función a ejecutar
def saludar(funcion):	
	#Definir el wrapper que recibe como parámetro el pasado por la función decorada 
    def saludo(*args, **kwargs):
        print("Buenos días")
        #Llamar a la función decorada
        funcion(*args, **kwargs)
        print("Hasta luego!")
    #Devolver el wrapper
    return saludo

#Definir función decoradora
@saludar
#Definir función decorada
def presentarse(nombre):
    print("Mi nombre es "+nombre)
    
#Definir función decoradora
@saludar
#Definir función decorada
def contarHistoria(titulo, autor):
    print("Quiero contarte una historia titulada '"+titulo+"' escrita por "+autor+" ....")
    
#Definir función decoradora
@saludar
#Definir función decorada
def mostrarEstado():
    print("Todo está bien")
    
#Llamar función decorada
presentarse("María")
contarHistoria("La isla del tesoro","Robert Louis Stevenson" )
mostrarEstado()

Wraps y Functools

La definición de los wrappers hecha como en los ejemplos anteriores tiene la desventaja de esconder, entre otros valores, el nombre original de la función decorada que está siendo ejecutada. Si ejecutamos el siguiente ejemplo podemos ver que el nombre de la función devuelta es el de el wrapper y no el de la función llamada.

def saludar(funcion):
	def saludo(*args, **kwargs):
		print("Buenos días")
		funcion(*args, **kwargs)
		print("Hasta luego!")
	return saludo

@saludar
def presentarse(nombre):
    print("Mi nombre es "+nombre)
    
print("El nombre de la función es " + presentarse.__name__)

Resultado:

El nombre de la función es saludo

Esto puede solucionarse utilizando el decorador functools.wraps brindado por Python que copia los datos perdidos de la función decorada que está siendo llamada.

from functools import wraps

def saludar(funcion):
	#Utilización del decorador wraps
	@wraps(funcion)
	def saludo(*args, **kwargs):
		print("Buenos días")
		funcion(*args, **kwargs)
		print("Hasta luego!")
	return saludo

@saludar
def presentarse(nombre):
    print("Mi nombre es "+nombre)    
print("El nombre de la función es " + presentarse.__name__)

Resultado:

El nombre de la función es presentarse

Espero que les sirva!


¿Preguntas? ¿Comentarios?

Si tenés dudas, o querés dejarnos tus comentarios y consultas, sumate al grupo de Telegram de la comunidad JuncoTIC!
¡Te esperamos!


Andrea Navarro

- Ingeniera en Informática - Docente universitaria - Investigadora