Saltar al contenido principal

Tutorial de Decoradores Python

En este tutorial aprenderás a implementar decoradores en Python.
Actualizado 4 dic 2024  · 11 min de lectura

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

Domina Python para la ciencia de datos y adquiere habilidades muy demandadas.
Empieza a Aprender Gratis

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 a message, una variable de su ámbito envolvente (outer_function).
  • Aunque outer_function haya terminado de ejecutarse, inner_function conserva el acceso a message.

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:

  1. El método __init__ inicializa el decorador con la función que se va a decorar.
  2. 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

Adquiere los conocimientos de programación que necesitan todos los desarrolladores de 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.

Temas

Más información sobre Python

curso

Writing Functions in Python

4 hr
91.3K
Learn to use best practices to write maintainable, reusable, complex functions with good documentation.
Ver detallesRight Arrow
Comienza El Curso
Ver másRight Arrow
Relacionado

tutorial

Tutorial de funciones de Python

Un tutorial sobre funciones en Python que cubre cómo escribir funciones, cómo invocarlas y mucho más.
Karlijn Willems's photo

Karlijn Willems

14 min

tutorial

Tutorial de cadenas en Python

En este tutorial, aprenderás todo sobre las cadenas de Python: trocearlas y encadenarlas, manipularlas y darles formato con la clase Formatter, cadenas f, plantillas y ¡mucho más!
Sejal Jaiswal's photo

Sejal Jaiswal

16 min

tutorial

Tutorial de visualización de datos con Python y Tableau

Aprende a utilizar Python para ampliar las funciones de visualización de datos de Tableau.
Abid Ali Awan's photo

Abid Ali Awan

15 min

tutorial

Python Seaborn Tutorial Para Principiantes: Empezar a visualizar datos

Este tutorial de Seaborn le introduce en los fundamentos de la visualización de datos estadísticos
Moez Ali's photo

Moez Ali

20 min

tutorial

Tutorial de list index() de Python

En este tutorial, aprenderás exclusivamente sobre la función index().
Sejal Jaiswal's photo

Sejal Jaiswal

6 min

tutorial

Sentencias IF, ELIF y ELSE de Python

En este tutorial, aprenderás exclusivamente sobre las sentencias if else de Python.
Sejal Jaiswal's photo

Sejal Jaiswal

9 min

See MoreSee More