Los decoradores (Decorators) son una parte importante de Python. En pocas palabras: son funciones que modifican la funcionalidad de otras funciones. Ayudan a que nuestro código sea más corto y más "Pythonic" (con estilo Python). La mayoría de los principiantes no saben dónde usarlos, así que voy a compartir en qué áreas los decoradores pueden hacer tu código más limpio. Primero, vamos a discutir cómo escribir tus propios decoradores.

Este es probablemente uno de los conceptos más difíciles de dominar. Lo discutiremos paso a paso, para que puedas entenderlo completamente.

Todo es un objeto

Primero, entendamos las funciones en Python:

def hi(name="yasoob"): return "hi " + name print(hi()) # output: 'hi yasoob' # Incluso podemos asignar una función a una variable, por ejemplo greet = hi # Aquí no estamos usando paréntesis, porque no estamos llamando a la función hi # Sino que la estamos colocando dentro de la variable greet. Intentemos ejecutar esto print(greet()) # output: 'hi yasoob' # Si eliminamos la función hi anterior, ¡veamos qué sucede! del hi print(hi()) #outputs: NameError print(greet()) #outputs: 'hi yasoob'

Definir funciones dentro de funciones

Eso son los conceptos básicos de las funciones. Llevemos tu conocimiento un paso más allá. En Python podemos definir otra función dentro de una función:

def hi(name="yasoob"): print("now you are inside the hi() function") def greet(): return "now you are in the greet() function" def welcome(): return "now you are in the welcome() function" print(greet()) print(welcome()) print("now you are back in the hi() function") hi() #output:now you are inside the hi() function # now you are in the greet() function # now you are in the welcome() function # now you are back in the hi() function # Lo anterior muestra que cada vez que llames a hi(), greet() y welcome() serán llamadas a la vez. # Luego, las funciones greet() y welcome() no son accesibles fuera de la función hi(), por ejemplo: greet() #outputs: NameError: name 'greet' is not defined

Ahora sabemos que podemos definir otras funciones dentro de una función. Es decir, podemos crear funciones anidadas. Ahora necesitas aprender un poco más: las funciones también pueden devolver funciones.

Devolver funciones desde una función

En realidad, no es necesario ejecutar otra función dentro de una función; también podemos devolverla como salida:

def hi(name="yasoob"): def greet(): return "now you are in the greet() function" def welcome(): return "now you are in the welcome() function" if name == "yasoob": return greet else: return welcome a = hi() print(a) #outputs: <function greet at 0x7f2143c01500> # Lo anterior muestra claramente que `a` ahora apunta a la función greet() dentro de la función hi() # Ahora prueba esto print(a()) #outputs: now you are in the greet() function

Mira este código otra vez. En la sentencia if/else devolvemos greet y welcome, no greet() y welcome(). ¿Por qué? Porque cuando pones un par de paréntesis después, la función se ejecuta; sin embargo, si no le pones paréntesis, se puede pasar por todas partes y asignarse a otras variables sin ejecutarla. ¿Lo entiendes? Déjame explicarlo con un poco más de detalle.

  • Cuando escribimosa = hi(),hi()se ejecutará, y como el parámetro name por defecto es yasoob, la funcióngreet()es devuelta.

  • También podemos imprimirhi()(), esto mostrará:now you are in the greet() function。

  • Si cambiamos la sentencia aa = hi(name = "ali"), entonceswelcome()la función será devuelta.

Pasar una función como argumento a otra función

def hi(): return "hi yasoob!" def doSomethingBeforeHi(func): print("I am doing some boring work before executing hi()") print(func()) doSomethingBeforeHi(hi) #outputs:I am doing some boring work before executing hi() # hi yasoob!

Ahora tienes todo el conocimiento necesario para aprender qué son realmente los decoradores. Los decoradores te permiten ejecutar código antes y después de una función.

Tu primer decorador

En el ejemplo anterior, ¡ya hemos creado un decorador! Ahora modifiquemos el decorador anterior y escribamos un programa un poco más útil:

def a_new_decorator(a_func): def wrapTheFunction(): print("I am doing some boring work before executing a_func()") a_func() print("I am doing some boring work after executing a_func()") return wrapTheFunction def a_function_requiring_decoration(): print("I am the function which needs some decoration to remove my foul smell") a_function_requiring_decoration() #outputs: "I am the function which needs some decoration to remove my foul smell" a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration) #now a_function_requiring_decoration is wrapped by wrapTheFunction() a_function_requiring_decoration() #outputs:I am doing some boring work before executing a_func() # I am the function which needs some decoration to remove my foul smell # I am doing some boring work after executing a_func()

¿Lo ves? Acabamos de aplicar los principios que aprendimos antes. ¡Esto es exactamente lo que hacen los decoradores en Python! Envuelven una función y modifican su comportamiento de una forma u otra. Ahora quizás te preguntes, en nuestro código no usamos el símbolo@¿símbolo? Eso es solo una forma abreviada de generar una función decorada. Aquí es cómo usamos@para ejecutar el código anterior:

@a_new_decorator def a_function_requiring_decoration(): """Hey you! Decorate me!""" print("I am the function which needs some decoration to " "remove my foul smell") a_function_requiring_decoration() #outputs: I am doing some boring work before executing a_func() # I am the function which needs some decoration to remove my foul smell # I am doing some boring work after executing a_func() #the @a_new_decorator is just a short way of saying: a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)

Espero que ahora tengas una comprensión básica de cómo funcionan los decoradores de Python. Si ejecutamos el siguiente código, hay un problema:

print(a_function_requiring_decoration.__name__)
# Output: wrapTheFunction

¡Esto no es lo que queríamos! La salida (Output) debería ser "a_function_requiring_decoration". Aquí la función fue reemplazada por warpTheFunction. Sobrescribió el nombre de nuestra función y la cadena de documentación (docstring). Afortunadamente, Python nos proporciona una función simple para resolver este problema: functools.wraps. Modifiquemos el ejemplo anterior para usar functools.wraps:

from functools import wraps def a_new_decorator(a_func): @wraps(a_func) def wrapTheFunction(): print("I am doing some boring work before executing a_func()") a_func() print("I am doing some boring work after executing a_func()") return wrapTheFunction @a_new_decorator def a_function_requiring_decoration(): """Hey yo! Decorate me!""" print("I am the function which needs some decoration to " "remove my foul smell") print(a_function_requiring_decoration.__name__) # Output: a_function_requiring_decoration

Ahora es mucho mejor. A continuación, aprendamos algunos casos de uso comunes de los decoradores.

Plantilla estándar:

from functools import wraps def decorator_name(f): @wraps(f) def decorated(*args, **kwargs): if not can_run: return "Function will not run" return f(*args, **kwargs) return decorated @decorator_name def func(): return("Function is running") can_run = True print(func()) # Output: Function is running can_run = False print(func()) # Output: Function will not run

Nota:@wrapsAcepta una función para decorarla y agrega funcionalidades como copiar el nombre de la función, la cadena de documentación, la lista de parámetros, etc. Esto nos permite acceder a los atributos de la función antes de ser decorada desde dentro del decorador.


Casos de uso

Ahora veamos dónde brillan especialmente los decoradores y cómo usarlos puede hacer que algunas cosas sean más fáciles de manejar.

Autorización (Authorization)

Los decoradores pueden ayudar a verificar si una persona está autorizada para usar un endpoint de una aplicación web. Se usan ampliamente en los frameworks web Flask y Django. Aquí hay un ejemplo de uso de autorización basada en decoradores:

from functools import wraps def requires_auth(f): @wraps(f) def decorated(*args, **kwargs): auth = request.authorization if not auth or not check_auth(auth.username, auth.password): authenticate() return f(*args, **kwargs) return decorated

Registro (Logging)

El registro (logging) es otro punto destacado del uso de decoradores. Este es un ejemplo:

from functools import wraps def logit(func): @wraps(func) def with_logging(*args, **kwargs): print(func.__name__ + " was called") return func(*args, **kwargs) return with_logging @logit def addition_func(x): """Do some math.""" return x + x result = addition_func(4) # Output: addition_func was called

Apuesto a que ya estás pensando en otro uso inteligente de los decoradores.


Decoradores con parámetros

Pensemos en esta cuestión: ¿no es @wraps también un decorador? Pero recibe un argumento, como cualquier función normal puede hacer. Entonces, ¿por qué no hacemos lo mismo? Esto es porque cuando usas la sintaxis @my_decorator, estás aplicando una función envolvente que toma una sola función como argumento. Recuerda, en Python todo es un objeto, ¡y esto incluye a las funciones! Con esto en mente, podemos escribir funciones que devuelvan una función envolvente.

Incrustando decoradores en funciones

Volvamos al ejemplo del registro y creemos una función envolvente que nos permita especificar un archivo de registro para la salida.

from functools import wraps def logit(logfile='out.log'): def logging_decorator(func): @wraps(func) def wrapped_function(*args, **kwargs): log_string = func.__name__ + " was called" print(log_string) # Abrir el archivo de registro y escribir el contenido with open(logfile, 'a') as opened_file: # Ahora envía el registro al archivo de registro especificado opened_file.write(log_string + '\n') return func(*args, **kwargs) return wrapped_function return logging_decorator @logit() def myfunc1(): pass myfunc1() # Output: myfunc1 was called # Ahora aparece un archivo llamado out.log, cuyo contenido es la cadena anterior @logit(logfile='func2.log') def myfunc2(): pass myfunc2() # Output: myfunc2 was called # Ahora aparece un archivo llamado func2.log, cuyo contenido es la cadena anterior

Clases de decoradores

Ahora tenemos un decorador logit apto para producción, pero cuando ciertas partes de nuestra aplicación todavía son frágiles, las excepciones pueden ser algo que requiera atención más urgente. Por ejemplo, a veces solo quieres registrar en un archivo. Pero a veces quieres enviar un correo electrónico con el problema que llama tu atención, manteniendo también el registro como constancia. Este es un escenario para usar herencia, pero hasta ahora solo hemos visto funciones para construir decoradores.

Afortunadamente, las clases también se pueden usar para construir decoradores. Entonces, ahora reconstruyamos logit usando una clase en lugar de una función.

from functools import wraps class logit(object): def __init__(self, logfile='out.log'): self.logfile = logfile def __call__(self, func): @wraps(func) def wrapped_function(*args, **kwargs): log_string = func.__name__ + " was called" print(log_string) # Abrir el archivo de registro y escribir with open(self.logfile, 'a') as opened_file: # Ahora envía el registro al archivo especificado opened_file.write(log_string + '\n') # Ahora, envía una notificación self.notify() return func(*args, **kwargs) return wrapped_function def notify(self): # logit solo registra, no hace nada más pass

Esta implementación tiene una ventaja adicional: es más limpia que el enfoque de funciones anidadas, y envuelve una función con la misma sintaxis de antes:

@logit()
def myfunc1():
    pass

Ahora, creemos una subclase de logit para añadir la funcionalidad de email (aunque el tema del email no se desarrollará aquí).

class email_logit(logit): '''Una versión de logit que puede enviar un email al administrador cuando se llama a la función''' def __init__(self, email='admin@myproject.com', *args, **kwargs): self.email = email super(email_logit, self).__init__(*args, **kwargs) def notify(self): # Enviar un email a self.email # Aquí no se implementa pass

A partir de ahora, @email_logit tendrá el mismo efecto que @logit, pero además de registrar, enviará un correo electrónico al administrador.

Dirección original: https://eastlakeside.gitbooks.io/interpy-zh/content/decorators/