9. Parámetros excesivos, banderas booleanas y firmas difíciles de usar

9.1 Objetivo del tema

La firma de una función es la primera forma de comunicación entre quien escribe el código y quien lo usa. Si una función recibe demasiados parámetros, banderas booleanas ambiguas o argumentos en un orden difícil de recordar, aumenta la posibilidad de errores.

En este tema aprenderemos a detectar firmas problemáticas y a reemplazarlas por alternativas más claras: argumentos nombrados, objetos de configuración, funciones separadas y estructuras de datos explícitas.

Objetivo práctico: mejorar funciones Python cuya firma es difícil de leer, recordar o usar correctamente.

9.2 Qué es una firma difícil de usar

Una firma difícil de usar obliga al programador a recordar detalles que el código debería expresar. Esto ocurre cuando hay muchos parámetros del mismo tipo, argumentos opcionales poco claros o valores booleanos que cambian mucho el comportamiento.

Ejemplo problemático:

total = calcular_total(3000, 2, "vip", "AR", True, False, True)

Al leer la llamada no sabemos qué significan True, False y True. Tampoco queda claro si 3000 es precio, subtotal, impuesto o límite.

9.3 Smell: demasiados parámetros

Una función con muchos parámetros suele indicar que recibe datos que pertenecen a un mismo concepto o que hace demasiadas cosas.

def crear_usuario(nombre, apellido, email, edad, pais, ciudad, activo, rol):
    return {
        "nombre": nombre,
        "apellido": apellido,
        "email": email,
        "edad": edad,
        "pais": pais,
        "ciudad": ciudad,
        "activo": activo,
        "rol": rol,
    }

La función no es larga, pero su firma ya es incómoda. Es fácil invertir pais y ciudad, olvidar un parámetro o pasar valores en orden incorrecto.

9.4 Usar argumentos nombrados

Una mejora inmediata es llamar a la función con argumentos nombrados. No cambia la firma, pero hace la llamada más explícita.

usuario = crear_usuario(
    nombre="Ana",
    apellido="Pérez",
    email="ana@example.com",
    edad=28,
    pais="AR",
    ciudad="Córdoba",
    activo=True,
    rol="cliente",
)

Los argumentos nombrados reducen errores de orden y hacen que la llamada se lea como datos estructurados.

9.5 Agrupar datos relacionados

Cuando varios parámetros pertenecen al mismo concepto, conviene agruparlos. Una opción simple es usar un diccionario con claves claras.

datos_usuario = {
    "nombre": "Ana",
    "apellido": "Pérez",
    "email": "ana@example.com",
    "edad": 28,
    "pais": "AR",
    "ciudad": "Córdoba",
    "activo": True,
    "rol": "cliente",
}

usuario = crear_usuario(datos_usuario)

Esto mejora la llamada, aunque todavía requiere cuidado: un diccionario no valida por sí mismo qué claves existen ni qué tipos se esperan.

9.6 Usar dataclasses para datos del dominio

Una alternativa más explícita es usar dataclass. Esto permite representar datos relacionados con una estructura clara.

from dataclasses import dataclass


@dataclass
class DatosUsuario:
    nombre: str
    apellido: str
    email: str
    edad: int
    pais: str
    ciudad: str
    activo: bool
    rol: str


def crear_usuario(datos: DatosUsuario):
    return {
        "nombre": datos.nombre,
        "apellido": datos.apellido,
        "email": datos.email,
        "edad": datos.edad,
        "pais": datos.pais,
        "ciudad": datos.ciudad,
        "activo": datos.activo,
        "rol": datos.rol,
    }

Más adelante profundizaremos en dataclasses. Por ahora nos interesa ver cómo ayudan a reducir firmas enormes.

9.7 Smell: banderas booleanas

Una bandera booleana cambia el comportamiento de una función según un True o False. No siempre está mal, pero puede ser una señal de que la función hace más de una cosa.

def generar_reporte(ventas, incluir_impuestos):
    total = sum(ventas)
    if incluir_impuestos:
        total = total * 1.21
    return total

La llamada puede ser ambigua:

total = generar_reporte(ventas, True)

Al leerla, no sabemos qué significa True sin ir a la definición de la función.

9.8 Mejorar llamadas con argumentos nombrados

Si la bandera booleana es razonable, al menos conviene usar argumento nombrado:

total = generar_reporte(ventas, incluir_impuestos=True)

Esto no elimina el smell por completo, pero reduce ambigüedad en la llamada.

9.9 Separar funciones cuando hay dos comportamientos

Si la bandera indica dos comportamientos distintos, suele ser más claro crear dos funciones.

def calcular_total_sin_impuestos(ventas):
    return sum(ventas)


def calcular_total_con_impuestos(ventas):
    total = calcular_total_sin_impuestos(ventas)
    return total * 1.21

La llamada ahora expresa la intención:

total = calcular_total_con_impuestos(ventas)

9.10 Banderas booleanas múltiples

Varias banderas booleanas juntas son una señal más fuerte de problema.

def exportar_reporte(datos, incluir_impuestos, incluir_descuentos, formato_pdf):
    # Mucha lógica condicional según las combinaciones de True y False.
    pass

Con tres banderas ya existen ocho combinaciones posibles. Algunas tal vez no tengan sentido, pero la firma permite llamarlas igual.

9.11 Usar opciones explícitas

Cuando hay varias opciones relacionadas, un objeto de configuración puede ser más claro.

from dataclasses import dataclass


@dataclass
class OpcionesReporte:
    incluir_impuestos: bool = True
    incluir_descuentos: bool = True
    formato: str = "pdf"


def exportar_reporte(datos, opciones: OpcionesReporte):
    if opciones.incluir_impuestos:
        datos = aplicar_impuestos(datos)
    if opciones.incluir_descuentos:
        datos = aplicar_descuentos(datos)
    return generar_archivo(datos, opciones.formato)

La firma queda más estable y las opciones se agrupan en un concepto con nombre propio.

9.12 Parámetros opcionales peligrosos

Un parámetro opcional puede ser útil, pero también puede ocultar caminos de ejecución muy distintos.

def guardar_datos(datos, ruta=None):
    if ruta is None:
        print(datos)
    else:
        with open(ruta, "w", encoding="utf-8") as archivo:
            archivo.write(str(datos))

Esta función imprime o escribe un archivo según un parámetro. Tal vez convenga separar dos funciones: una para mostrar y otra para guardar.

9.13 Firmas con parámetros del mismo tipo

Cuando varios parámetros son del mismo tipo, es fácil invertirlos sin que Python lo detecte.

def calcular_periodo(fecha_inicio, fecha_fin, fecha_pago):
    return {
        "inicio": fecha_inicio,
        "fin": fecha_fin,
        "pago": fecha_pago,
    }

Esta llamada puede compilar aunque esté mal ordenada:

periodo = calcular_periodo(fecha_pago, fecha_inicio, fecha_fin)

En estos casos, los argumentos nombrados o una estructura de datos reducen mucho el riesgo.

9.14 Aplicación sobre ventas_demo

En el proyecto ventas_demo, la función principal puede tener una firma similar a esta:

def calcular_total_venta(productos, cliente, pais):
    ...

Por ahora es razonable, pero podría complicarse si agregamos más opciones:

def calcular_total_venta(
    productos,
    cliente,
    pais,
    incluir_impuestos,
    incluir_envio,
    redondear,
):
    ...

Si aparecen esos parámetros, conviene detenerse y diseñar una alternativa más clara antes de que la firma crezca sin control.

9.15 Una alternativa con opciones de cálculo

Podemos agrupar opciones en una estructura explícita:

from dataclasses import dataclass


@dataclass
class OpcionesCalculo:
    incluir_impuestos: bool = True
    incluir_envio: bool = True
    redondear: bool = True


def calcular_total_venta(productos, cliente, pais, opciones=None):
    if opciones is None:
        opciones = OpcionesCalculo()

    subtotal = calcular_subtotal(productos)
    descuento = obtener_descuento(cliente)
    total = subtotal * (1 - descuento)

    if opciones.incluir_impuestos:
        total = aplicar_impuesto(total, pais)
    if opciones.incluir_envio:
        total = aplicar_envio(total)
    if opciones.redondear:
        total = round(total, 2)

    return total

Esta versión concentra las opciones en un objeto. Aun así, si cada opción activa caminos muy distintos, tal vez convenga separar casos de uso.

9.16 Evitar mutables como valores por defecto

En Python, usar listas o diccionarios como valores por defecto es un error común que puede producir comportamiento inesperado.

Evita esto:

def agregar_producto(producto, productos=[]):
    productos.append(producto)
    return productos

Prefiere esto:

def agregar_producto(producto, productos=None):
    if productos is None:
        productos = []
    productos.append(producto)
    return productos

Este problema es de comportamiento, pero también afecta calidad porque la firma parece inocente y es fácil de usar mal.

9.17 Keyword-only arguments

Python permite obligar a que ciertos argumentos se pasen por nombre usando * en la firma.

def calcular_total_venta(productos, *, cliente, pais):
    subtotal = calcular_subtotal(productos)
    descuento = obtener_descuento(cliente)
    impuesto = obtener_impuesto(pais)
    return round(subtotal * (1 - descuento) * (1 + impuesto), 2)

Ahora esta llamada es explícita:

total = calcular_total_venta(productos, cliente="vip", pais="AR")

Y esta llamada no es válida:

total = calcular_total_venta(productos, "vip", "AR")

9.18 Ejercicio guiado

Analiza esta función:

def enviar_email(destino, asunto, cuerpo, urgente, copia, html):
    if urgente:
        asunto = "[URGENTE] " + asunto
    if copia:
        destino = destino + "," + copia
    if html:
        cuerpo = f"<html><body>{cuerpo}</body></html>"
    return f"Para: {destino}\nAsunto: {asunto}\n{cuerpo}"

Problemas posibles:

  • Demasiados parámetros.
  • Banderas booleanas que cambian el comportamiento.
  • Responsabilidades mezcladas: prioridad, copia, formato y armado del mensaje.
  • El valor de copia puede ser ambiguo si no hay copia.

9.19 Una posible mejora

from dataclasses import dataclass


@dataclass
class Email:
    destino: str
    asunto: str
    cuerpo: str
    copia: str | None = None
    es_urgente: bool = False
    formato: str = "texto"


def preparar_asunto(email):
    if email.es_urgente:
        return f"[URGENTE] {email.asunto}"
    return email.asunto


def preparar_destino(email):
    if email.copia:
        return f"{email.destino},{email.copia}"
    return email.destino


def preparar_cuerpo(email):
    if email.formato == "html":
        return f"<html><body>{email.cuerpo}</body></html>"
    return email.cuerpo


def enviar_email(email):
    destino = preparar_destino(email)
    asunto = preparar_asunto(email)
    cuerpo = preparar_cuerpo(email)
    return f"Para: {destino}\nAsunto: {asunto}\n{cuerpo}"

La mejora no consiste solo en usar una clase de datos. También separamos pequeñas decisiones para que cada parte tenga un nombre claro.

9.20 Ejercicio propuesto

Mejora la firma de esta función sin cambiar el comportamiento esperado:

def registrar_pago(usuario, importe, moneda, fecha, enviar_recibo, aplicar_descuento, guardar_log):
    if aplicar_descuento:
        importe = importe * 0.9
    mensaje = f"{usuario} pagó {importe} {moneda} el {fecha}"
    if enviar_recibo:
        mensaje = mensaje + " - recibo enviado"
    if guardar_log:
        mensaje = "[LOG] " + mensaje
    return mensaje

Realiza estas tareas:

  • Identifica qué parámetros pertenecen a datos del pago.
  • Identifica qué parámetros son opciones de comportamiento.
  • Propón una dataclass para datos o para opciones.
  • Usa argumentos nombrados en las llamadas de ejemplo.
  • Agrega una prueba antes y después de la mejora.

9.21 Lista de verificación

Antes de continuar, verifica que puedes hacer lo siguiente:

  • Reconocer funciones con demasiados parámetros.
  • Detectar banderas booleanas ambiguas.
  • Usar argumentos nombrados para mejorar llamadas.
  • Agrupar datos relacionados con diccionarios o dataclasses.
  • Separar funciones cuando una bandera representa otro comportamiento.
  • Evitar valores mutables como parámetros por defecto.
  • Usar argumentos obligatoriamente nombrados cuando mejora la claridad.

9.22 Conclusión

En este tema vimos que una firma difícil de usar también es un code smell. Muchos parámetros, banderas booleanas y valores opcionales ambiguos aumentan el riesgo de llamadas incorrectas y dificultan las pruebas.

En el próximo tema estudiaremos la duplicación de código y la duplicación de conocimiento, dos problemas que suelen crecer silenciosamente en proyectos reales.