AulaML Python · C++
Python intermedio · 38 min · Funciones

Argumentos flexibles y alcance de variables

Recibir cualquier número de datos, entender dónde vive cada nombre y no compartir listas sin querer

Con *args y **kwargs una función acepta cualquier cantidad de argumentos posicionales o con nombre. Además aprenderás cómo Python resuelve cada nombre con la regla LEGB, cuándo una función modifica el objeto del llamador y por qué una lista por defecto es una trampa.

Lección 2 de 3 del módulo
Al terminar esta lección podrás
  • Escribir funciones que acepten un número variable de argumentos
  • Leer y usar las formas *args, **kwargs y los parámetros keyword-only
  • Desempaquetar listas y diccionarios al llamar a una función
  • Explicar la regla LEGB y cuándo hace falta global o nonlocal
  • Evitar el error clásico de la lista mutable como valor por defecto
  • Distinguir reasignar un parámetro de mutar el objeto recibido

Antes necesitas: Funciones: parámetros y retorno

En esta lección
  1. ¿Qué son *args, **kwargs y el alcance?
  2. ¿Para qué sirve?
  3. ¿Cómo funciona por dentro?
  4. Sintaxis: de la firma a la llamada
  5. Ejemplo básico: sumar lo que llegue
  6. Ejemplo intermedio: opciones con nombre
  7. Caso práctico: un inventario que se actualiza
  8. Visualización: global, local y retorno
  9. Errores comunes
  10. Buenas prácticas
  11. Ejercicios
  12. Soluciones explicadas
  13. Resumen
  14. Evaluación

La lección anterior te dio funciones con una firma fija: dos parámetros, dos argumentos. Pero las bibliotecas reales no funcionan así. print acepta cualquier cantidad de valores, dict acepta pares con nombre y un modelo de scikit-learn recibe decenas de opciones que casi nunca usas todas. Necesitas una forma de decir «recibe lo que venga» sin escribir veinte variantes de la misma función.

En esta lección vas a aprender las dos herramientas que resuelven eso —*args y **kwargs— y, sobre todo, a entender dónde vive cada nombre. El alcance explica por qué una variable creada dentro de una función no existe fuera, por qué a veces una función sí modifica tu lista y por qué un valor por defecto aparentemente inocente puede arrastrar datos entre llamadas.

¿Qué son *args, **kwargs y el alcance?

*args recoge en una tupla todos los argumentos posicionales que sobran. **kwargs recoge en un diccionario todos los argumentos con nombre que no coinciden con un parámetro declarado. Los nombres args y kwargs son convención; lo que importa son los asteriscos, y puedes llamarlos *numeros o **opciones si describe mejor su contenido.

El alcance (scope) es la zona del programa donde un nombre es visible. Cada llamada a una función crea un espacio propio de nombres. Python lo busca en un orden fijo —local, envolvente, global, incorporado— y se detiene en el primero que encuentre. Esa regla se conoce por las siglas LEGB.

¿Para qué sirve?

  • Funciones de resumen: sumar(*numeros) o promedio(*notas) sin fijar cuántos valores llegan.
  • Envoltorios y decoradores: una función que recibe *args, **kwargs, los pasa tal cual y añade algo alrededor.
  • Opciones de configuración: crear_modelo(datos, *, profundidad=3, learning_rate=0.1) deja claro qué se puede ajustar.
  • Registro de eventos: registrar("compra", usuario="ana", monto=120) guarda datos heterogéneos sin definir un parámetro por campo.
  • Llamadas dinámicas: pasar una lista con * o un diccionario con ** a una función que espera argumentos separados.

¿Cómo funciona por dentro?

Al llamar a una función, Python enlaza los argumentos en este orden: primero los posicionales a los parámetros posicionales; después los que vienen con nombre a su parámetro; luego los posicionales sobrantes van a *args; los nombrados sobrantes, a **kwargs. Si un obligatorio queda sin valor, la llamada falla antes de ejecutar una sola línea del cuerpo.

Un parámetro situado después de *args —o después de un * solitario— es keyword-only: solo se puede pasar con nombre. Del mismo modo, los parámetros anteriores a **kwargs pueden ser posicionales o con nombre, pero **kwargs captura todo lo demás. Esta gramática permite diseñar firmas muy expresivas sin perder validación.

Forma en el defRecibeEjemplo de llamada
def f(a, b)Exactamente dos posicionalesf(1, 2)
def f(a=0)Uno opcional, se puede omitirf() o f(3)
def f(*args)Tupla con todos los posicionalesf(1, 2, 3)
def f(**kwargs)Diccionario con todos los nombradosf(x=1, y=2)
def f(a, *, b)a posicional o nombrado; b solo nombradof(1, b=2)

Para los nombres rige LEGB: Local (el marco actual), Enclosing (funciones que envuelven a la actual), Global (el módulo) y Built-in (len, print, sum). La primera coincidencia gana. Si dentro de una función asignas a un nombre, Python lo considera local en toda la función, incluso antes de la asignación; de ahí el UnboundLocalError que verás más adelante.

nonlocal aparece en funciones anidadas: permite modificar una variable del ámbito envolvente sin tocar el global. Se declara igual que global y solo funciona si el nombre ya existe en la función exterior.

Sintaxis: de la firma a la llamada

Los asteriscos funcionan en los dos sentidos: al definir, empaquetan; al llamar, desempaquetan. *lista reparte sus elementos como argumentos posicionales y **diccionario reparte sus claves como argumentos con nombre. El número de claves y valores debe coincidir con lo que la función espera.

PYTHON firmas.py
# *extras recoge posicionales; nota es keyword-only; **datos recoge el resto
def pedido(producto, *extras, nota="", **datos):
    """Muestra un pedido con extras y datos opcionales."""
    print(producto, extras, nota, datos)


pedido("café", "leche", "canela", nota="sin azúcar", mesa=4)

# Desempaquetado al llamar
extras = ("leche", "canela")
opciones = {"nota": "sin azúcar", "mesa": 4}
pedido("café", *extras, **opciones)
  • Puedes combinar *args y **kwargs en la misma firma, en ese orden.
  • Un * sin nombre (def f(a, *, b)) no recoge nada: solo marca que lo siguiente es keyword-only.
  • Al llamar, * y ** deben ir después de los argumentos explícitos.
  • Un diccionario pasado con ** no puede repetir un parámetro ya asignado: obtendrías TypeError: f() got multiple values for argument 'a'.
  • Los nombres de **kwargs deben ser identificadores válidos; f(**{"a-b": 1}) falla.

Ejemplo básico: sumar lo que llegue

PYTHON sumar.py
def sumar(*numeros):
    """Suma cualquier cantidad de números."""
    total = 0
    for n in numeros:
        total += n
    return total

print(sumar(3, 5))
print(sumar(1, 2, 3, 4))
print(sumar())

La línea 1 declara *numeros: todo lo que llegue posicionalmente se empaqueta en una tupla. En la primera llamada, numeros es (3, 5); en la segunda, (1, 2, 3, 4). El bucle de las líneas 4 y 5 acumula en total, que empieza en 0.

La última llamada no pasa nada, así que numeros es la tupla vacía y el bucle no se ejecuta: devuelve 0. Esa es una decisión de diseño razonable para una suma. Si en cambio quieres exigir al menos un valor, añade una guarda al principio.

Ejemplo intermedio: opciones con nombre

Este ejemplo mezcla las tres formas. titulo es obligatorio y posicional; ancho y borde son keyword-only con valor por defecto; **opciones guarda cualquier dato adicional sin que la función tenga que conocerlo de antemano.

PYTHON reporte.py
def crear_reporte(titulo, *, ancho=20, borde="-", **opciones):
    """Devuelve un reporte enmarcado y las opciones no usadas."""
    linea = borde * ancho
    texto = f"{linea}\n{titulo}\n{linea}"
    return texto, opciones


texto, extras = crear_reporte("Ventas", ancho=10, borde="=", autor="Ana")
print(texto)
print(extras)

ancho=10 y borde="=" se enlazan con sus parámetros porque tienen nombre; autor="Ana" no coincide con ninguno y cae en opciones. El resultado del print es un marco de diez signos igual, la palabra Ventas y otro marco; el diccionario muestra {'autor': 'Ana'}.

Si llamaras crear_reporte("Ventas", 10), Python lanzaría TypeError: crear_reporte() takes 1 positional argument but 2 were given: el * de la firma prohíbe pasar ancho por posición. Esa restricción es deliberada, porque crear_reporte("Ventas", 10, 20) no dice qué significa cada número.

Caso práctico: un inventario que se actualiza

En una tienda, varias funciones añaden productos al mismo inventario. Aquí aparece la diferencia entre un objeto mutable compartido y una lista nueva. Lee el ejemplo pensando en qué lista se modifica en cada llamada.

PYTHON inventario.py
def agregar(producto, inventario=None):
    """Devuelve el inventario con el producto añadido."""
    if inventario is None:
        inventario = []
    inventario.append(producto)
    return inventario


stock = ["arroz"]
agregar("fideos", stock)
print(stock)
print(agregar("sal"))
print(agregar("azúcar"))

inventario=None no significa «lista vacía»: significa «no se pasó nada». La guarda de la línea 3 crea una lista nueva en cada llamada sin argumento, y por eso agregar("sal") y agregar("azúcar") producen listas independientes. Si el valor por defecto hubiera sido [], ambas llamadas compartirían la misma lista y la segunda devolvería ['sal', 'azúcar'].

La línea 10 no reasigna stock, pero lo modifica: append trabaja sobre el objeto que ambos nombres comparten. Ese es el paso por referencia de objeto de Python. Si dentro de la función escribieras inventario = [producto], la variable externa seguiría intacta, porque solo habrías cambiado a qué apunta el nombre local.

Visualización: global, local y retorno

La siguiente traza muestra una función que incrementa un contador global. El panel de la pila tiene dos marcos: el módulo, donde vive contador, y registrar, donde vive puntos. Observa que global no copia el valor: autoriza a la función a escribir en el nombre del módulo.

Ejecución visual ·Quién modifica a quién
1 / 8
1 contador = 0
2
3
4 def registrar(puntos):
5 global contador
6 contador += 1
7 return puntos + contador
8
9
10 print(registrar(10))
11 print(contador)
Se crea contador en el ámbito global con el valor 0.
Variables
contador = 0
Salida
—

Avanza paso a paso: observa la línea resaltada, las variables y la salida.

Sin global, la línea 6 habría intentado escribir en una variable local llamada contador y habría lanzado UnboundLocalError, porque Python la considera local en toda la función. Usar global está justificado en contadores sencillos; en programas grandes conviene devolver el nuevo valor o encapsular el estado en un objeto.

Errores comunes

Mensaje o síntomaPor qué ocurreCómo se arregla
TypeError: f() got an unexpected keyword argument 'x'La función no declara x ni tiene **kwargsAñade el parámetro o acepta **kwargs
UnboundLocalError: local variable 'total' referenced before assignmentAsignas a un nombre global dentro de la función sin globalDeclara global o pasa el valor como parámetro
SyntaxError: name 'x' is assigned to before global declarationglobal x aparece después de usar xColoca la declaración al inicio de la función
Dos llamadas comparten datos en una lista por defectoEl valor por defecto se crea una sola vez, al definirUsa None y crea la lista dentro
TypeError: f() takes 0 positional arguments but 1 was givenLlamaste con un posicional a una firma keyword-onlyPasa el valor con nombre: f(x=1)
La lista por defecto que nunca se vacía
Incorrecto
PYTHON Comparte estado
def agregar(producto, lista=[]):
    lista.append(producto)
    return lista

print(agregar("sal"))
print(agregar("azúcar"))
Correcto
PYTHON Correcto
def agregar(producto, lista=None):
    if lista is None:
        lista = []
    lista.append(producto)
    return lista

print(agregar("sal"))
print(agregar("azúcar"))

El valor por defecto se evalúa una única vez, cuando se ejecuta el `def`; todas las llamadas que lo omiten reciben el mismo objeto. La primera versión imprime `['sal']` y luego `['sal', 'azúcar']`. La segunda crea una lista nueva en cada llamada sin argumento y devuelve `['sal']` y `['azúcar']`.

Buenas prácticas

  • Nombra los parámetros variables por su contenido: *notas, **opciones, no *a ni **k.
  • Usa * para hacer keyword-only las opciones booleanas: def crear(datos, *, normalizar=False) se lee sin ambigüedad.
  • Documenta en la docstring qué claves espera **kwargs; si son muchas y fijas, declara parámetros normales.
  • Nunca uses una lista, un diccionario o un conjunto como valor por defecto: usa None y créalo dentro.
  • Evita global salvo para constantes de configuración o contadores muy locales; devolver valores es más testeable.
  • Si una función modifica la lista que recibe, dilo en el nombre o en la docstring: agregar_en_sitio.
  • Al desempaquetar con **, comprueba que las claves existan como parámetros; un error tipográfico se convierte en TypeError.
Modificar una lista: Python y C++
Python
PYTHON referencia.py
def agregar_ultimo(lista):
    lista.append(lista[-1])
    return lista

datos = [1, 2]
agregar_ultimo(datos)
print(datos)
C++
CPP referencia.cpp
#include <iostream>
#include <vector>

void agregar_ultimo(std::vector<int>& lista) {
    lista.push_back(lista.back());
}

int main() {
    std::vector<int> datos{1, 2};
    agregar_ultimo(datos);
    std::cout << datos.size() << std::endl;
    return 0;
}

En Python todo argumento es una referencia al objeto, así que `append` modifica la lista del llamador sin declarar nada especial. En C++ hay que pedirlo explícitamente con `&`; sin él, la función recibe una copia. La contrapartida es que en Python reasignar el parámetro (`lista = [9]`) no cambia la variable externa, porque solo mueve el nombre local.

Ejercicios

Ejercicio 1

Resumen de ventas

Escribe resumen(*ventas) que devuelva una tupla con la cantidad de ventas, el total y el promedio. Si no recibe nada, debe devolver (0, 0, 0) sin lanzar errores. Prueba con tres importes y después con una lista desempaquetada con *.
Pista

len(ventas) y sum(ventas) funcionan sobre la tupla. Para el promedio, divide solo cuando la cantidad sea mayor que cero.

Completar código Completa la firma flexible
def mostrar(*, **):
    print(args, kwargs)

mostrar(1, 2, nombre="Ada")

Los nombres son convención, pero el cuerpo del ejemplo ya los usa: deben coincidir exactamente.

Ejercicio 2

Ficha de usuario con opciones

Escribe crear_usuario(nombre, *, rol="alumno", **extra) que devuelva un diccionario con nombre, rol y todos los pares adicionales. Llama una vez solo con el nombre y otra con rol="docente" y sede="Lima". Muestra ambos diccionarios.
Pista

Crea el diccionario base y actualízalo con extra usando update. El * de la firma obliga a escribir rol= con nombre.

Soluciones explicadas

En el primer ejercicio, *ventas no es un detalle decorativo: cambia el contrato de la función. Sin él necesitarías una lista como único argumento y el llamador tendría que envolver los valores. Con él, ambas formas conviven gracias al desempaquetado, y la tupla resultante es idéntica. La guarda convierte el caso vacío en un resultado predecible en lugar de una excepción.

En el segundo, el * separa lo que identifica al usuario (nombre, obligatorio) de lo que lo configura (rol, con valor por defecto) y de lo que es libre (extra). Esa jerarquía evita llamadas ambiguas y permite que la función crezca sin romper a quienes ya la usan. Cuando el número de claves de extra crezca, la docstring debe enumerarlas.

Resumen

  • *args empaqueta los posicionales sobrantes en una tupla; **kwargs, los nombrados en un diccionario.
  • Los nombres son convención: lo que importa es el asterisco y el uso coherente en el cuerpo.
  • Un parámetro después de * es keyword-only y solo se pasa con nombre.
  • Al llamar, *lista y **diccionario desempaquetan sus elementos como argumentos.
  • LEGB: Python busca en local, envolvente, global y built-in, y usa la primera coincidencia.
  • global y nonlocal permiten escribir en un ámbito exterior; sin ellas, asignar crea una local.
  • Los valores por defecto se evalúan una sola vez: nunca pongas listas o diccionarios mutables.
  • Python pasa referencias a objetos: mutar el objeto afecta al llamador; reasignar el nombre, no.

Evaluación

Evaluación 1

Dentro de def f(*args), ¿qué tipo tiene args?

Evaluación 2

¿Qué imprime este código? ``python def f(x, lista=[]): lista.append(x) return lista print(f(1)) print(f(2)) ``

Evaluación 3

En x = 10 global y una función que hace x = x + 1 sin declarar global, ¿qué ocurre al llamarla?

Tus notas

¿Ya lo entendiste?

Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.