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.
- 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
- ¿Qué son *args, **kwargs y el alcance?
- ¿Para qué sirve?
- ¿Cómo funciona por dentro?
- Sintaxis: de la firma a la llamada
- Ejemplo básico: sumar lo que llegue
- Ejemplo intermedio: opciones con nombre
- Caso práctico: un inventario que se actualiza
- Visualización: global, local y retorno
- Errores comunes
- Buenas prácticas
- Ejercicios
- Soluciones explicadas
- Resumen
- 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)opromedio(*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 def | Recibe | Ejemplo de llamada |
|---|---|---|
def f(a, b) | Exactamente dos posicionales | f(1, 2) |
def f(a=0) | Uno opcional, se puede omitir | f() o f(3) |
def f(*args) | Tupla con todos los posicionales | f(1, 2, 3) |
def f(**kwargs) | Diccionario con todos los nombrados | f(x=1, y=2) |
def f(a, *, b) | a posicional o nombrado; b solo nombrado | f(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.
# *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
*argsy**kwargsen 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íasTypeError: f() got multiple values for argument 'a'. - Los nombres de
**kwargsdeben ser identificadores válidos;f(**{"a-b": 1})falla.
Ejemplo básico: sumar lo que llegue
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.
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.
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.
contador = 0def registrar(puntos): global contador contador += 1 return puntos + contadorprint(registrar(10))print(contador)contador en el ámbito global con el valor 0.—
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íntoma | Por qué ocurre | Cómo se arregla |
|---|---|---|
TypeError: f() got an unexpected keyword argument 'x' | La función no declara x ni tiene **kwargs | Añade el parámetro o acepta **kwargs |
UnboundLocalError: local variable 'total' referenced before assignment | Asignas a un nombre global dentro de la función sin global | Declara global o pasa el valor como parámetro |
SyntaxError: name 'x' is assigned to before global declaration | global x aparece después de usar x | Coloca la declaración al inicio de la función |
| Dos llamadas comparten datos en una lista por defecto | El valor por defecto se crea una sola vez, al definir | Usa None y crea la lista dentro |
TypeError: f() takes 0 positional arguments but 1 was given | Llamaste con un posicional a una firma keyword-only | Pasa el valor con nombre: f(x=1) |
def agregar(producto, lista=[]):
lista.append(producto)
return lista
print(agregar("sal"))
print(agregar("azúcar"))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*ani**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
Noney créalo dentro. - Evita
globalsalvo 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 enTypeError.
def agregar_ultimo(lista):
lista.append(lista[-1])
return lista
datos = [1, 2]
agregar_ultimo(datos)
print(datos)#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
Resumen de ventas
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.
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.
Ficha de usuario con opciones
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
*argsempaqueta 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,
*listay**diccionariodesempaquetan sus elementos como argumentos. - LEGB: Python busca en local, envolvente, global y built-in, y usa la primera coincidencia.
globalynonlocalpermiten 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
Dentro de def f(*args), ¿qué tipo tiene args?
¿Qué imprime este código?
``python
def f(x, lista=[]):
lista.append(x)
return lista
print(f(1))
print(f(2))
``
En x = 10 global y una función que hace x = x + 1 sin declarar global, ¿qué ocurre al llamarla?
¿Ya lo entendiste?
Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.