Cómo usar múltiples decoradores en una sola función de Python

PythonPythonBeginner
Practicar Ahora

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Los decoradores de Python son una herramienta poderosa que te permite modificar el comportamiento de las funciones sin cambiar su código fuente. En este tutorial, exploraremos cómo usar múltiples decoradores en una sola función de Python, abriendo un mundo de posibilidades para la reutilización de código y la mejora de funciones.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/arguments_return("Arguments and Return Values") python/FunctionsGroup -.-> python/lambda_functions("Lambda Functions") python/AdvancedTopicsGroup -.-> python/decorators("Decorators") python/AdvancedTopicsGroup -.-> python/context_managers("Context Managers") subgraph Lab Skills python/function_definition -.-> lab-397700{{"Cómo usar múltiples decoradores en una sola función de Python"}} python/arguments_return -.-> lab-397700{{"Cómo usar múltiples decoradores en una sola función de Python"}} python/lambda_functions -.-> lab-397700{{"Cómo usar múltiples decoradores en una sola función de Python"}} python/decorators -.-> lab-397700{{"Cómo usar múltiples decoradores en una sola función de Python"}} python/context_managers -.-> lab-397700{{"Cómo usar múltiples decoradores en una sola función de Python"}} end

Comprender los decoradores de Python

¿Qué son los decoradores en Python?

Los decoradores en Python son una forma poderosa y flexible de modificar el comportamiento de una función o una clase sin cambiar su código fuente. Son una forma de "envolver" una función con otra función, lo que permite que la función envolvente ejecute código antes y/o después de que se llame a la función original.

¿Por qué usar decoradores?

Los decoradores son útiles para una variedad de tareas, como:

  • Registrar llamadas a funciones
  • Almacenar en caché los resultados de funciones
  • Aplicar control de acceso
  • Medir el tiempo de ejecución de una función
  • Volver a intentar llamadas a funciones fallidas

¿Cómo funcionan los decoradores?

Los decoradores en Python se definen utilizando el símbolo @, seguido de la función decoradora. La función decoradora toma una función como argumento, realiza algún procesamiento adicional y luego devuelve una nueva función que se puede llamar en lugar de la función original.

A continuación, se muestra un ejemplo sencillo de una función decoradora que registra los argumentos pasados a una función:

def log_args(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args} and kwargs={kwargs}")
        return func(*args, **kwargs)
    return wrapper

@log_args
def add_numbers(a, b):
    return a + b

result = add_numbers(2, 3)
print(result)

Esto generará la siguiente salida:

Calling add_numbers with args=(2, 3) and kwargs={}
5

Decoradores anidados

Los decoradores se pueden anidar, lo que te permite aplicar múltiples decoradores a una sola función. El orden en el que se aplican los decoradores es importante, ya que determina el orden en el que se ejecutan las funciones decoradoras.

def uppercase(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

def reverse(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result[::-1]
    return wrapper

@uppercase
@reverse
def greet(name):
    return f"Hello, {name}!"

print(greet("LabEx"))

Esto generará la siguiente salida:

!XEbal,OLLEH

Argumentos de los decoradores

Los decoradores también pueden tomar argumentos, lo que te permite personalizar el comportamiento del decorador. Esto es útil cuando quieres crear un decorador que se pueda configurar de diferentes maneras.

def repeat(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            result = ""
            for _ in range(n):
                result += func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    return f"Hello, {name}!"

print(greet("LabEx"))

Esto generará la siguiente salida:

Hello, LabEx!Hello, LabEx!Hello, LabEx!

Aplicar múltiples decoradores

El orden de aplicación de los decoradores

Cuando aplicas múltiples decoradores a una función, el orden en el que se aplican es importante. Los decoradores se aplican de abajo hacia arriba, lo que significa que el decorador más interno se aplica primero y el decorador más externo se aplica último.

def uppercase(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

def reverse(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result[::-1]
    return wrapper

@uppercase
@reverse
def greet(name):
    return f"Hello, {name}!"

print(greet("LabEx"))

Esto generará la siguiente salida:

!XEBAL,OLLEH

Apilar decoradores

También puedes apilar múltiples decoradores en una sola función aplicándolos uno después del otro. Esto es equivalente a anidar los decoradores, pero puede hacer que el código sea más legible.

def uppercase(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

def reverse(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result[::-1]
    return wrapper

def greet(name):
    return f"Hello, {name}!"

greet_upper_reverse = uppercase(reverse(greet))
print(greet_upper_reverse("LabEx"))

Esto generará la siguiente salida:

!XEBAL,OLLEH

Decorar métodos

Los decoradores también se pueden utilizar para modificar el comportamiento de los métodos en una clase. Se aplican los mismos principios, pero la función decoradora debe tomar el parámetro self como primer argumento.

def log_method(func):
    def wrapper(self, *args, **kwargs):
        print(f"Calling {func.__name__} on {self.__class__.__name__} with args={args} and kwargs={kwargs}")
        return func(self, *args, **kwargs)
    return wrapper

class Person:
    def __init__(self, name):
        self.name = name

    @log_method
    def greet(self, message):
        return f"{message}, {self.name}!"

person = Person("LabEx")
print(person.greet("Hello"))

Esto generará la siguiente salida:

Calling greet on Person with args=('Hello',) and kwargs={}
Hello, LabEx!

Decorar clases

Los decoradores también se pueden utilizar para modificar el comportamiento de clases enteras. En este caso, la función decoradora toma una clase como argumento y devuelve una nueva clase con el comportamiento deseado.

def singleton(cls):
    instances = {}

    def wrapper(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return wrapper

@singleton
class MyClass:
    def __init__(self, value):
        self.value = value

obj1 = MyClass(42)
obj2 = MyClass(24)

print(obj1 is obj2)  ## True
print(obj1.value)    ## 42
print(obj2.value)    ## 42

En este ejemplo, el decorador singleton asegura que solo se cree una instancia de la clase MyClass, independientemente de cuántas veces se instancie la clase.

Ejemplos de decoradores en el mundo real

Decorador de registro (Logging Decorator)

Un caso de uso común de los decoradores es el registro de llamadas a funciones. Esto puede ser útil para depuración, monitoreo o auditoría.

def log_function_call(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args} and kwargs={kwargs}")
        return func(*args, **kwargs)
    return wrapper

@log_function_call
def add_numbers(a, b):
    return a + b

result = add_numbers(2, 3)
print(result)

Esto generará la siguiente salida:

Calling add_numbers with args=(2, 3) and kwargs={}
5

Decorador de caché (Caching Decorator)

Los decoradores también se pueden utilizar para almacenar en caché los resultados de llamadas a funciones costosas, mejorando el rendimiento.

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n <= 1:
        return n
    else:
        return (fibonacci(n-1) + fibonacci(n-2))

print(fibonacci(100))

El decorador lru_cache del módulo functools proporciona una forma sencilla de implementar una caché de uso menos reciente (Least Recently Used - LRU) para los resultados de las funciones.

Decorador de control de acceso (Access Control Decorator)

Los decoradores se pueden utilizar para aplicar control de acceso a funciones o métodos, asegurando que solo los usuarios autorizados puedan acceder a cierta funcionalidad.

from functools import wraps

def require_admin(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        if not is_admin(args[0]):
            raise ValueError("Access denied. You must be an admin.")
        return func(*args, **kwargs)
    return wrapper

class User:
    def __init__(self, name, is_admin):
        self.name = name
        self.is_admin = is_admin

    @require_admin
    def delete_user(self, user_to_delete):
        print(f"Deleting user: {user_to_delete.name}")

admin = User("LabEx", True)
regular_user = User("John", False)

admin.delete_user(regular_user)  ## Works
regular_user.delete_user(admin)  ## Raises ValueError

En este ejemplo, el decorador require_admin verifica si el usuario que llama al método delete_user es un administrador antes de permitir que la operación se realice.

Decorador de reintentos (Retry Decorator)

Los decoradores también se pueden utilizar para implementar un mecanismo de reintentos para funciones que pueden fallar debido a problemas temporales, como errores de red o límites de tasa de la API.

import time
from functools import wraps

def retry(max_retries=3, delay=1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            retries = 0
            while retries < max_retries:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"Function {func.__name__} failed. Retrying... ({retries+1}/{max_retries})")
                    retries += 1
                    time.sleep(delay)
            raise Exception(f"Maximum number of retries ({max_retries}) reached for function {func.__name__}")
        return wrapper
    return decorator

@retry(max_retries=3, delay=2)
def flaky_function():
    ## Simulate a flaky function that fails 50% of the time
    if random.random() < 0.5:
        raise Exception("Oops, something went wrong!")
    return "Success!"

print(flaky_function())

En este ejemplo, el decorador retry reintentará automáticamente la función flaky_function hasta 3 veces, con un retraso de 2 segundos entre cada intento, antes de lanzar una excepción.

Estos son solo algunos ejemplos de los muchos casos de uso reales de los decoradores en Python. Los decoradores son una herramienta poderosa y flexible que puede ayudarte a escribir código más modular, mantenible y reutilizable.

Resumen

Al final de este tutorial, tendrás una comprensión sólida de cómo aplicar múltiples decoradores a una sola función de Python. Aprenderás ejemplos prácticos y casos de uso reales, lo que te permitirá escribir código Python más modular, flexible y mantenible.