105 - Decoradores con Argumentos

En el concepto anterior vimos como crear decoradores simples que pueden envolver funciones y ejecutar código antes o después de ellas.

Pero, ¿qué pasa cuando queremos que nuestros decoradores sean configurables, es decir, que acepten parámetros propios? Aquí es donde entran los decoradores con argumentos.

Un decorador normal solo recibe la función a decorar:

def mi_decorador(func):
    def envoltura(*args, **kwargs):
        print("Antes de la función")
        resultado = func(*args, **kwargs)
        print("Después de la función")
        return resultado
    return envoltura

Si queremos pasar parámetros adicionales al decorador, necesitamos un nivel extra de funciones. Esto se debe a que Python primero evalúa los paréntesis del decorador antes de aplicarlo a la función.

def decorador_parametrizado(mensaje):
    def decorador(func):
        def envoltura(*args, **kwargs):
            print(f"{mensaje} - Antes de la función")
            resultado = func(*args, **kwargs)
            print(f"{mensaje} - Después de la función")
            return resultado
        return envoltura
    return decorador

Primer nivel (decorador_parametrizado): recibe los argumentos que queremos pasar al decorador.

Segundo nivel (decorador): recibe la función a decorar.

Tercer nivel (envoltura): se ejecuta cuando llamamos a la función decorada y puede manipular sus argumentos o resultados.

Programa: ejercicio355.py

def decorador_parametrizado(mensaje):
    def decorador(func):
        def envoltura(*args, **kwargs):
            print(f"{mensaje} - Antes de la función")
            resultado = func(*args, **kwargs)
            print(f"{mensaje} - Después de la función")
            return resultado
        return envoltura
    return decorador

@decorador_parametrizado("Depuracion")
def principal():
    print("Función principal ejecutándose.")

principal()
@decorador_parametrizado("Depuracion")
def principal():
    print("Función principal ejecutándose.")

Python primero evalúa decorador_parametrizado("Depuracion"), que devuelve la función decorador.

Luego aplica decorador(principal), que devuelve envoltura.

Por lo tanto, la función principal queda reemplazada por envoltura.

Llamada a la función decorada

principal()

Cuando llamamos principal(), en realidad se ejecuta la función envoltura:
Imprime:

Depuracion - Antes de la función

Ejecuta la función original principal(), que imprime:

Función principal ejecutándose.

Imprime:

Depuracion - Después de la función

Problema

Creación de un decorador que tenga dos parámetros que limite valores numéricos entre un rango.

Programa: ejercicio356.py

def limitar_valores(min_val, max_val):
    """Decorador que asegura que todos los argumentos numéricos estén entre min_val y max_val."""
    def decorador(func):
        def envoltura(*args, **kwargs):
            for i, arg in enumerate(args):
                if not (arg >=min_val and arg <= max_val):
                    raise ValueError(f"Argumento {arg} en posición {i} fuera del rango [{min_val}, {max_val}]")            
            print(f"Todos los argumentos dentro del rango [{min_val}, {max_val}], ejecutando {func.__name__}...")
            return func(*args, **kwargs)
        return envoltura
    return decorador

# Uso del decorador
@limitar_valores(0, 100)
def multiplicar(a, b):
    return a * b

# Llamadas válidas
print(multiplicar(10, 5))    # 50
print(multiplicar(0, 100))   # 0

# Llamada inválida (lanza ValueError)
print(multiplicar(150, 5))  # ValueError
def limitar_valores(min_val, max_val):
    """Decorador que asegura que todos los argumentos numéricos estén entre min_val y max_val."""

Esta es la función externa del decorador parametrizado.
Recibe dos parámetros: min_val y max_val, que definen el rango permitido para los argumentos de la función decorada.

Devuelve el decorador real que se aplicará a la función.

Decorador interno

def decorador(func):

Esta función interna recibe la función que vamos a decorar (func).

Su tarea es envolver a func con lógica adicional (en este caso, verificar que los valores estén en rango).

Devuelve la función envoltura.

Función envoltura

def envoltura(*args, **kwargs):

Esta es la función que reemplaza a la función original cuando usamos el decorador.

Recibe cualquier número de argumentos posicionales (*args) y de palabras clave (**kwargs).

Dentro de envoltura se añade la lógica de validación:

for i, arg in enumerate(args):
    if not (arg >=min_val and arg <= max_val):
        raise ValueError(f"Argumento {arg} en posición {i} fuera del rango [{min_val}, {max_val}]")            

Recorre cada argumento posicional (args).

Si algún argumento está fuera del rango [min_val, max_val], lanza un ValueError.

Esto asegura que la función solo se ejecute con valores válidos.

Luego imprime un mensaje de confirmación:

print(f"Todos los argumentos dentro del rango [{min_val}, {max_val}], ejecutando {func.__name__}...")

Y finalmente ejecuta la función original:

return func(*args, **kwargs)

Problema

Creación una función que reciba como parámetro el nombre del jugador, luego tira dos dados y muestra el mensaje que gano si suma 7 y perdió en caso contrario. Crear una función decoradora que grabe en un archivo que se le pasa a la función decoradora el resultado de la tirada de los dos dados.

Programa: ejercicio357.py

import random

def guardar_en_archivo(nombre_archivo):
    """Decorador que guarda la salida de la función en un archivo de texto con jugador, dados y resultado."""
    def decorador(func):
        def envoltura(*args, **kwargs):
            dado1, dado2 = func(*args, **kwargs)
            total = dado1 + dado2
            if total == 7:
                resultado = "GANO"
            else:
                resultado = "PERDIO"
            with open(nombre_archivo, "a") as f:
                # Guardar nombre del jugador, valores de los dados y resultado
                f.write(f"{args[0]} {dado1} {dado2} {resultado}\n")
            print(f"{args[0]} {resultado} - Resultado guardado en {nombre_archivo}")
            return dado1, dado2, resultado
        return envoltura
    return decorador

# Función más interesante: lanzar dos dados
@guardar_en_archivo("partidas.txt")
def jugar_dados(jugador):
    """Simula lanzar 2 dados y devuelve los valores de cada dado."""
    dado1 = random.randint(1, 6)
    dado2 = random.randint(1, 6)
    print(f"{jugador} lanzó los dados: {dado1} + {dado2} = {dado1 + dado2}")
    return dado1, dado2

# Llamadas de ejemplo
jugar_dados("Diego")
jugar_dados("Carlos")

Definición del decorador parametrizado

def guardar_en_archivo(nombre_archivo):
    """Decorador que guarda la salida de la función en un archivo de texto con jugador, dados y resultado."""

Este decorador acepta un parámetro, nombre_archivo, que indica dónde se guardarán los resultados.

Es un decorador parametrizado, es decir, es una función que devuelve un decorador.

Decorador interno

def decorador(func):

Esta función recibe la función que será decorada (func).

Su objetivo es envolver la función original con comportamiento extra (guardar en archivo y calcular si ganó o perdió).

Función envoltura

def envoltura(*args, **kwargs):

Esta es la función que reemplaza a la original cuando la llamamos.

Recibe todos los argumentos posicionales (*args) y nombreados (**kwargs).

Llamar a la función original y calcular resultado

dado1, dado2 = func(*args, **kwargs)
total = dado1 + dado2
if total == 7:
    resultado = "GANO"
else:
    resultado = "PERDIO"

Se llama a la función original (jugar_dados) y se obtienen los valores de los dados.
Se calcula la suma (total).
Se determina si el jugador ganó (total == 7) o perdió (total != 7).

Guardar en archivo

with open(nombre_archivo, "a") as f:
    f.write(f"{args[0]} {dado1} {dado2} {resultado}\n")

Se abre el archivo en modo append ("a"), para no borrar lo anterior.

Se escribe una línea con el nombre del jugador (args[0]), los valores de los dados y el resultado.

Ejemplo de línea:
Diego 4 2 PERDIO

Imprimir mensaje de confirmación y devolver resultado

print(f"{args[0]} {resultado} - Resultado guardado en {nombre_archivo}")
return dado1, dado2, resultado

Se muestra un mensaje en pantalla confirmando que se guardó el resultado.

La función decorada devuelve los valores de los dados y el resultado, por si se quiere usar después en el programa.

Función que simula lanzar los dados

@guardar_en_archivo("partidas.txt")
def jugar_dados(jugador):
    dado1 = random.randint(1, 6)
    dado2 = random.randint(1, 6)
    print(f"{jugador} lanzó los dados: {dado1} + {dado2} = {dado1 + dado2}")
    return dado1, dado2

Se usa el decorador @guardar_en_archivo("partidas.txt").

jugar_dados devuelve los valores de los dos dados.

También imprime en pantalla la tirada de cada jugador.

Llamadas de ejemplo

jugar_dados("Diego")
jugar_dados("Carlos")

Cada llamada ejecuta envoltura, que:

Llama a jugar_dados.
Calcula si ganó o perdió.
Guarda la información en partidas.txt.
Imprime un mensaje de confirmación.

Ejemplo de contenido de partidas.txt después de dos partidas

Diego 2 3 PERDIO
Carlos 6 1 GANO

Cada línea corresponde a una tirada, mostrando jugador, valores de los dados y resultado.