Funciones con número variable de argumentos (*args)

¿Por qué usar args?

A veces queremos crear funciones que reciban un número variable de argumentos.

Por ejemplo, supón esta función sencilla 👇

def multiplicacion(x, y):
    return x * y

Si intentamos pasar más de dos argumentos:

multiplicacion(2, 3, 4)

⛔ Python nos dirá:

TypeError: multiplicacion() takes 2 positional arguments but 3 were given

🧩 La solución: usar un asterisco *

def multiplicacion(x, y):
    return x * y

🧩 La solución: usar un asterisco *

def multiplicar(*numeros):
    print(numeros)

Podemos reemplazar los parámetros fijos (x, y) por uno solo, precedido de un asterisco

Cuando usamos el asterisco, todos los argumentos que se pasen se agrupan dentro de una tupla.

📦 ¿Qué es lo que obtenemos?

multiplicar(2, 3, 4, 5)

⬇️ Salida esperada:

(2, 3, 4, 5)

🧠 Lo que obtenemos es una tupla, similar a una lista,

pero inmutable (no se puede modificar).

Por eso se escribe entre paréntesis () en lugar de corchetes [].

🔁 Recorriendo los valores

Como numeros es una tupla iterable, podemos recorrerla con un bucle for 👇

def multiplicar(*numeros):
    for n in numeros:
        print("Número:", n)

⬇️ Salida:

Número: 2
Número: 3
Número: 4
Número: 5

✨ Calculando el producto

Vamos a usar una variable acumuladora llamada total, que comienza en 1 y se multiplica por cada número.

def multiplicar(*numeros):
    total = 1
    for n in numeros:
        total *= n     # igual que total = total * n
    return total

⬇️ Ejemplo de uso:

print(multiplicar(1, 2, 3, 4, 5))
# 120

⚠️ Cuidado con la indentación

Un error común es poner return dentro del for, así 👇

def multiplicar(*numeros):
    total = 1
    for n in numeros:
        total *= n
        return total   # ❌ mal: sale en la primera vuelta

🧩 Debe ir fuera del bucle, al mismo nivel de indentación:

⚠️ Cuidado con la indentación

Un error común es poner return dentro del for, así 👇

def multiplicar(*numeros):
    total = 1
    for n in numeros:
        total *= n
    return total       # ✅ correcto

🧮 Producto vacío o error

¿Qué pasa si nadie pasa números?

multiplicar()

Podemos decidir entre dos comportamientos:

✅ Matemático: retornar 1 ❌ Defensivo: lanzar un error

def multiplicar(*numeros):
    if len(numeros) == 0:
        raise ValueError("Debes pasar al menos un número")
    total = 1
    for n in numeros:
        total *= n
    return total

🪄 Un atajo en Python 3.8+

La librería estándar incluye math.prod() para calcular el producto de un iterable directamente.

import math

def multiplicar(*numeros):
    return math.prod(numeros)

🧠 Es más corto, más rápido y hace exactamente lo mismo.

🧰 Desempaquetando una lista

Si ya tienes tus números en una lista o tupla, puedes desempaquetarlos con * al llamar la función 👇

numeros = [2, 3, 4]
print(multiplicar(*numeros))
# 24

💬 El mismo símbolo * empaqueta al definir y desempaqueta al llamar.

💬 Bonus: **kwargs (nombre + valor)

De forma similar, **kwargs permite recibir argumentos con nombre, que se guardan en un diccionario 🗂️

def demo(*args, **kwargs):
    print("args:", args)
    print("kwargs:", kwargs)

demo(1, 2, 3, base=10, sep="-")

⬇️ Salida:

args: (1, 2, 3)
kwargs: {'base': 10, 'sep': '-'}

🧩 ¿Para qué sirve **kwargs?

El parámetro **kwargs te permite crear funciones flexibles,
que aceptan parámetros opcionales con nombre 🧠

Esto es muy útil cuando no sabes exactamente qué opciones
puede necesitar el usuario o cuando quieres dar valores personalizados.

💬 Ejemplo práctico

Queremos una función que salude, pero que también permita personalizar el saludo.

def saludar(nombre, **opciones):
    saludo = f"Hola, {nombre}"
    if opciones.get("emoji"):
        saludo += " " + opciones["emoji"]
    if opciones.get("mayusculas"):
        saludo = saludo.upper()
    print(saludo)

🚀 Probemos diferentes llamadas

saludar("Danna")
saludar("Danna", emoji="🐍")
saludar("Danna", emoji="💫", mayusculas=True)

⬇️ Salida esperada:

Hola, Danna
Hola, Danna 🐍
HOLA, DANNA 💫

💡 Cada parámetro con nombre (emoji, mayusculas) se almacena dentro del diccionario kwargs.

🔍 Observa cómo se ve internamente

Agreguemos un print para ver qué llega a la función:

def saludar(nombre, **opciones):
    print(opciones)
    saludo = f"Hola, {nombre}"
    if opciones.get("emoji"):
        saludo += " " + opciones["emoji"]
    if opciones.get("mayusculas"):
        saludo = saludo.upper()
    print(saludo)
saludar("Danna", emoji="🌟", mayusculas=True)

⬇️ Salida:

{'emoji': '🌟', 'mayusculas': True}
HOLA, DANNA 🌟

🧠 Recuerda la diferencia

Tipo Símbolo Qué guarda Ejemplo recibido
*args * Tupla con valores sin nombre (1, 2, 3)
**kwargs ** Diccionario con nombre y valor {'emoji': '🌟'}

🔸 Usa *args cuando los valores no tienen nombre 🔸 Usa **kwargs cuando los valores tienen nombre

🧰 Combinando ambos

Podemos usar *args y **kwargs juntos. Por ejemplo, una función que recibe datos sueltos y opciones nombradas:

def mostrar_datos(*args, **kwargs):
    print("args:", args)
    print("kwargs:", kwargs)

mostrar_datos(1, 2, 3, nombre="Danna", curso="Python")

⬇️ Salida:

args: (1, 2, 3)
kwargs: {'nombre': 'Danna', 'curso': 'Python'}

💡 ¿Cuándo se usa el doble asterisco **?

El doble asterisco (**) se usa cuando queremos desempaquetar un diccionario y pasarlo como argumentos con nombre a una función 🗝️

Esto ocurre cuando ya tenemos los valores guardados
en un diccionario y queremos usarlos sin escribir cada uno manualmente.

📦 Ejemplo básico

Supón esta función:

def presentar(nombre, edad):
    print(f"Me llamo {nombre} y tengo {edad} años.")

Y tenemos un diccionario con la información 👇

datos = {"nombre": "Danna", "edad": 28}
presentar(**datos)

⬇️ Salida:

Me llamo Danna y tengo 28 años.

🧠 Python toma las claves del diccionario como nombres de parámetros, y los valores como los datos que se pasan a la función.

🧠 Comparación visual

Forma Qué hace Ejemplo
*lista Desempaqueta valores sin nombre f(*[1, 2])
**diccionario Desempaqueta pares nombre=valor f(**{"x":1, "y":2})

🔹 Usa * cuando tengas una secuencia (lista, tupla) 🔹 Usa ** cuando tengas un diccionario con nombres de variables

🌟 Cierre del tema

*args → empaqueta/desempaqueta valores sueltos**kwargs → empaqueta/desempaqueta valores con nombre** al llamar → toma claves y valores de un diccionario

Con esto ya dominas todas las formas en que Python puede recibir y expandir argumentos 🐍💫