curso
Tutorial de Decoradores Python
Un decorador es un patrón de diseño en Python que permite al usuario añadir nuevas funciones a un objeto existente sin modificar su estructura. Los decoradores suelen aplicarse a las funciones, y desempeñan un papel crucial a la hora de mejorar o modificar el comportamiento de las funciones. Tradicionalmente, los decoradores se colocan antes de la definición de una función que quieras decorar. En este tutorial, demostraremos cómo utilizar eficazmente los decoradores en las funciones de Python.
Las funciones en Python son ciudadanos de primera clase. Esto significa que admiten operaciones como ser pasados como argumento, devueltos por una función, modificados y asignados a una variable. Esta propiedad es crucial, ya que permite que las funciones se traten como cualquier otro objeto en Python, lo que permite una mayor flexibilidad en la programación.
Para ejecutar fácilmente tú mismo todo el código de ejemplo de este tutorial, puedes crear gratuitamente un libro de trabajo DataLab que tenga Python preinstalado y contenga todos los ejemplos de código. Para practicar más con los decoradores, consulta este ejercicio práctico de DataCamp.
Aprende Python desde cero
Asignar funciones a variables
Para empezar, creamos una función que sumará uno a un número cada vez que sea llamada. A continuación, asignaremos la función a una variable y utilizaremos esta variable para llamar a la función.
def plus_one(number):
return number + 1
add_one = plus_one
add_one(5)
6
Definir funciones dentro de otras funciones
A continuación, ilustraremos cómo puedes definir una función dentro de otra función en Python. Quédate conmigo, pronto descubriremos cómo todo esto es relevante para crear y comprender los decoradores en Python.
def plus_one(number):
def add_one(number):
return number + 1
result = add_one(number)
return result
plus_one(4)
5
Pasar funciones como argumentos a otras funciones
Las funciones también pueden pasarse como parámetros a otras funciones. Ilustrémoslo a continuación.
def plus_one(number):
return number + 1
def function_call(function):
number_to_add = 5
return function(number_to_add)
function_call(plus_one)
6
Funciones que devuelven otras funciones
Una función también puede generar otra función. Te lo mostraremos a continuación con un ejemplo.
def hello_function():
def say_hi():
return "Hi"
return say_hi
hello = hello_function()
hello()
'Hi'
Comprender los cierres
Python permite que una función anidada acceda al ámbito externo de la función que la encierra. Se trata de un concepto crítico en los decoradores, conocido como cierre.
Un cierre en Python es una función que recuerda el entorno en el que fue creada, incluso después de que ese entorno ya no esté activo. Esto significa que una función anidada puede "cerrar sobre" variables de su ámbito envolvente y seguir utilizándolas.
Los cierres son esenciales para entender los decoradores, porque éstos dependen de la capacidad de una función envolvente anidada para acceder y modificar el estado de la función decoradora que los encierra.
Ejemplo de cierre:
def outer_function(message):
def inner_function():
print(f"Message from closure: {message}")
return inner_function
closure_function = outer_function("Hello, closures!")
closure_function()
# Output: Message from closure: Hello, closures!
En este ejemplo:
inner_function
es un cierre porque accede amessage
, una variable de su ámbito envolvente (outer_function
).- Aunque
outer_function
haya terminado de ejecutarse,inner_function
conserva el acceso amessage
.
Cuando creas un decorador, la función envolvente (dentro del decorador) es un cierre. Conserva el acceso a la función decorada y a cualquier estado o argumento adicional definido en la función decoradora. Por ejemplo:
def simple_decorator(func):
def wrapper():
print("Before the function call")
func()
print("After the function call")
return wrapper
@simple_decorator
def greet():
print("Hello!")
greet()
# Output:
# Before the function call
# Hello!
# After the function call
Aquí, wrapper
es un cierre que recuerda la función greet
y añade un comportamiento antes y después de su ejecución.
Crear decoradores
Con estos requisitos previos fuera del camino, vamos a crear un sencillo decorador que convierta una frase a mayúsculas. Lo hacemos definiendo una envoltura dentro de una función adjunta. Como puedes ver es muy similar a la función dentro de otra función que hemos creado antes.
def uppercase_decorator(function):
def wrapper():
func = function()
make_uppercase = func.upper()
return make_uppercase
return wrapper
Nuestra función decoradora toma una función como argumento, por lo que definiremos una función y se la pasaremos a nuestro decorador. Antes aprendimos que podíamos asignar una función a una variable. Utilizaremos ese truco para llamar a nuestra función decoradora.
def say_hi():
return 'hello there'
decorate = uppercase_decorator(say_hi)
decorate()
'HELLO THERE'
Sin embargo, Python nos proporciona una forma mucho más sencilla de aplicar decoradores. Simplemente utilizamos el símbolo @ delante de la función que queremos decorar. Vamos a demostrarlo en la práctica a continuación.
@uppercase_decorator
def say_hi():
return 'hello there'
say_hi()
'HELLO THERE'
Aplicar varios decoradores a una misma función
Podemos utilizar varios decoradores para una misma función. Sin embargo, los decoradores se aplicarán en el orden en que los hayamos llamado. A continuación definiremos otro decorador que divide la frase en una lista. A continuación, aplicaremos el decorador uppercase_decorator
y split_string
a una única función.
import functools
def split_string(function):
@functools.wraps(function)
def wrapper():
func = function()
splitted_string = func.split()
return splitted_string
return wrapper
@split_string
@uppercase_decorator
def say_hi():
return 'hello there'
say_hi()
['HELLO', 'THERE']
De la salida anterior, observamos que la aplicación de los decoradores es de abajo arriba. Si hubiéramos intercambiado el orden, habríamos visto un error, ya que las listas no tienen el atributo upper
. Primero se ha convertido la frase a mayúsculas y luego se ha dividido en una lista.
Nota: Al apilar decoradores, es una práctica habitual utilizar functools.wraps
para garantizar que los metadatos de la función original se conservan durante todo el proceso de apilamiento. Esto ayuda a mantener la claridad y la coherencia en la depuración y la comprensión de las propiedades de la función decorada.
Aceptar argumentos en funciones de decorador
A veces podemos necesitar definir un decorador que acepte argumentos. Lo conseguimos pasando los argumentos a la función envoltorio. Los argumentos se pasarán a la función que se esté decorando en el momento de la llamada.
def decorator_with_arguments(function):
def wrapper_accepting_arguments(arg1, arg2):
print("My arguments are: {0}, {1}".format(arg1,arg2))
function(arg1, arg2)
return wrapper_accepting_arguments
@decorator_with_arguments
def cities(city_one, city_two):
print("Cities I love are {0} and {1}".format(city_one, city_two))
cities("Nairobi", "Accra")
My arguments are: Nairobi, Accra
Cities I love are Nairobi and Accra
Nota: Es esencial asegurarse de que el número de argumentos del decorador (arg1, arg2
en este ejemplo) coincide con el número de argumentos de la función envuelta (cities
en este ejemplo). Esta alineación es crucial para evitar errores y garantizar una funcionalidad adecuada al utilizar decoradores con argumentos.
Definición de decoradores de uso general
Para definir un decorador de propósito general que pueda aplicarse a cualquier función, utilizamos args
y **kwargs
. args
y **kwargs
recogen todos los argumentos posicionales y de palabra clave y los almacenan en las variables args y kwargs. args
y kwargs
nos permiten pasar tantos argumentos como queramos durante las llamadas a funciones.
def a_decorator_passing_arbitrary_arguments(function_to_decorate):
def a_wrapper_accepting_arbitrary_arguments(*args,**kwargs):
print('The positional arguments are', args)
print('The keyword arguments are', kwargs)
function_to_decorate(*args)
return a_wrapper_accepting_arbitrary_arguments
@a_decorator_passing_arbitrary_arguments
def function_with_no_argument():
print("No arguments here.")
function_with_no_argument()
The positional arguments are ()
The keyword arguments are {}
No arguments here.
Veamos cómo utilizaríamos el decorador utilizando argumentos posicionales.
@a_decorator_passing_arbitrary_arguments
def function_with_arguments(a, b, c):
print(a, b, c)
function_with_arguments(1,2,3)
The positional arguments are (1, 2, 3)
The keyword arguments are {}
1 2 3
Los argumentos de las palabras clave se pasan utilizando palabras clave. A continuación se muestra un ejemplo.
@a_decorator_passing_arbitrary_arguments
def function_with_keyword_arguments():
print("This has shown keyword arguments")
function_with_keyword_arguments(first_name="Derrick", last_name="Mwiti")
The positional arguments are ()
The keyword arguments are {'first_name': 'Derrick', 'last_name': 'Mwiti'}
This has shown keyword arguments
Nota: El uso de **kwargs
en el decorador le permite manejar argumentos de palabras clave. Esto hace que el decorador de propósito general sea versátil y capaz de manejar diversos tipos de argumentos durante las llamadas a funciones.
Pasar argumentos al decorador
Veamos ahora cómo pasar argumentos al propio decorador. Para conseguirlo, definimos un creador de decoradores que acepte argumentos y, a continuación, definimos un decorador dentro de él. A continuación, definimos una función envolvente dentro del decorador, como hicimos anteriormente.
def decorator_maker_with_arguments(decorator_arg1, decorator_arg2, decorator_arg3):
def decorator(func):
def wrapper(function_arg1, function_arg2, function_arg3) :
"This is the wrapper function"
print("The wrapper can access all the variables\n"
"\t- from the decorator maker: {0} {1} {2}\n"
"\t- from the function call: {3} {4} {5}\n"
"and pass them to the decorated function"
.format(decorator_arg1, decorator_arg2,decorator_arg3,
function_arg1, function_arg2,function_arg3))
return func(function_arg1, function_arg2,function_arg3)
return wrapper
return decorator
pandas = "Pandas"
@decorator_maker_with_arguments(pandas, "Numpy","Scikit-learn")
def decorated_function_with_arguments(function_arg1, function_arg2,function_arg3):
print("This is the decorated function and it only knows about its arguments: {0}"
" {1}" " {2}".format(function_arg1, function_arg2,function_arg3))
decorated_function_with_arguments(pandas, "Science", "Tools")
The wrapper can access all the variables
- from the decorator maker: Pandas Numpy Scikit-learn
- from the function call: Pandas Science Tools
and pass them to the decorated function
This is the decorated function, and it only knows about its arguments: Pandas Science Tools
Depuración de decoradores
Como hemos visto, los decoradores envuelven funciones. El nombre de la función original, su docstring y la lista de parámetros quedan ocultos por el cierre envolvente: Por ejemplo, cuando intentemos acceder a los metadatos de decorated_function_with_arguments
, veremos los metadatos del cierre envoltorio. Esto supone un reto a la hora de depurar.
decorated_function_with_arguments.__name__
'wrapper'
decorated_function_with_arguments.__doc__
'This is the wrapper function'
Para resolver este problema, Python proporciona un decorador functools.wraps
. Este decorador copia los metadatos perdidos de la función no decorada al cierre decorado. Vamos a mostrar cómo lo haríamos.
import functools
def uppercase_decorator(func):
@functools.wraps(func)
def wrapper():
return func().upper()
return wrapper
@uppercase_decorator
def say_hi():
"This will say hi"
return 'hello there'
say_hi()
'HELLO THERE'
Cuando comprobamos los metadatos de say_hi
, observamos que ahora se refiere a los metadatos de la función y no a los del envoltorio.
say_hi.__name__
'say_hi'
say_hi.__doc__
'This will say hi'
Es aconsejable y una buena práctica utilizar siempre functools.wraps
al definir los decoradores. Te ahorrará muchos quebraderos de cabeza en la depuración.
Decoradores basados en clases
Aunque los decoradores basados en funciones son habituales, Python también te permite crear decoradores basados en clases, que proporcionan mayor flexibilidad y facilidad de mantenimiento, especialmente para casos de uso complejos. Un decorador basado en una clase es una clase con un método __call__
que le permite comportarse como una función.
class UppercaseDecorator:
def __init__(self, function):
self.function = function
def __call__(self, *args, **kwargs):
result = self.function(*args, **kwargs)
return result.upper()
@UppercaseDecorator
def greet():
return "hello there"
print(greet())
# Output: HELLO THERE
Cómo funciona:
- El método
__init__
inicializa el decorador con la función que se va a decorar. - El método
__call__
se invoca cuando se llama a la función decorada, lo que permite al decorador modificar su comportamiento.
Ventajas de los decoradores basados en clases:
- Decoradores con estado: Los decoradores basados en clases pueden mantener el estado mediante variables de instancia, a diferencia de los decoradores basados en funciones, que requieren cierres o variables globales.
- Legibilidad: Para los decoradores complejos, encapsular la lógica en una clase puede hacer que el código esté más organizado y sea más fácil de entender.
Ejemplo de decorador con estado:
class CallCounter:
def __init__(self, function):
self.function = function
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Function {self.function.__name__} has been called {self.count} times.")
return self.function(*args, **kwargs)
@CallCounter
def say_hello():
print("Hello!")
say_hello()
say_hello()
# Output:
# Function say_hello has been called 1 times.
# Hello!
# Function say_hello has been called 2 times.
# Hello!
Caso práctico real del Decorador: Almacenamiento en caché
El decorador lru_cache
es una herramienta incorporada en Python que almacena en caché los resultados de las llamadas a funciones caras. Esto mejora el rendimiento al evitar cálculos redundantes para entradas repetidas.
Ejemplo:
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(50)) # Subsequent calls with the same argument are much faster
Otros usos comunes de los decoradores:
-
Registro: Rastrea las llamadas a funciones, los argumentos y los valores de retorno para depurar o auditar.
-
Autentificación: Aplica el control de acceso en aplicaciones web como Flask o Django.
-
Tiempo de ejecución: Mide y optimiza el tiempo de ejecución de las funciones para las tareas de rendimiento crítico.
-
Mecanismo de reintento: Reintenta automáticamente las llamadas a funciones fallidas, útil en operaciones de red.
-
Validación de la entrada: Valida los argumentos de la función antes de ejecutarla.
Resumen de los Decoradores de Python
Los decoradores modifican dinámicamente la funcionalidad de una función, método o clase sin tener que utilizar directamente subclases ni cambiar el código fuente de la función que se está decorando. Utilizar decoradores en Python también garantiza que tu código sea DRY(Don't Repeat Yourself). Los decoradores tienen varios casos de uso, como
- Autorización en frameworks de Python como Flask y Django
- Registro
- Medir el tiempo de ejecución
- Sincronización
Para saber más sobre los decoradores de Python, consulta la Biblioteca de Decoradores de Python.
Conviértete en Desarrollador Python
Preguntas frecuentes
¿Hay consideraciones de rendimiento al utilizar decoradores?
Sí, los decoradores pueden añadir sobrecarga porque introducen llamadas a funciones adicionales. Cuando el rendimiento es crítico, es importante tener en cuenta esta sobrecarga, especialmente si la función decorada se llama con frecuencia en un contexto sensible al rendimiento.
¿Se pueden utilizar decoradores con métodos de clase y, en caso afirmativo, cómo?
Sí, los decoradores pueden aplicarse a los métodos de las clases igual que las funciones normales. El decorador recibirá el método como argumento y devolverá un nuevo método o una versión modificada del método. Se suele utilizar para el registro, el control de acceso o el cumplimiento de condiciones previas.
¿Cómo se pueden utilizar los decoradores para el registro?
Los decoradores pueden utilizarse para registrar las llamadas a funciones, sus argumentos y valores de retorno, envolviendo la ejecución de la función con código que registre estos detalles en un sistema de registro. Esto ayuda a rastrear y depurar.
¿Qué significa el símbolo @ en los decoradores?
El símbolo@
es azúcar sintáctico en Python que simplifica la aplicación de un decorador a una función. Te permite aplicar un decorador a una función directamente encima de su definición, haciendo el código más limpio y legible.
¿Puede un decorador modificar el valor de retorno de una función, y cómo funcionaría?
Sí, un decorador puede modificar el valor de retorno de una función alterando la sentencia de retorno dentro de la función envolvente. Por ejemplo, podría transformar el tipo de datos de salida, formatearlo o añadir un procesamiento adicional antes de devolver el resultado final.
¿Cómo gestiona Python el ámbito de las variables cuando una función anidada accede a una variable desde su función anexa?
Python utiliza una regla de ámbito LEGB (Local, Enclosing, Global, Built-in). En el caso de las funciones anidadas, la función anidada puede acceder a variables del ámbito de su función envolvente, lo que permite cierres en los que la función interna conserva el acceso a las variables de la función externa incluso después de que ésta haya terminado de ejecutarse.
Más información sobre Python
curso
Writing Efficient Python Code
curso
Introduction to Functions in Python
tutorial
Tutorial de funciones de Python
tutorial
Tutorial de cadenas en Python
tutorial
Tutorial de visualización de datos con Python y Tableau
tutorial
Python Seaborn Tutorial Para Principiantes: Empezar a visualizar datos
tutorial
Tutorial de list index() de Python
tutorial