En la lección anterior, nuestras funciones recibían datos de la forma más simple: un valor por posición para cada parámetro. Suficiente para empezar, pero corto para Papyrus: el cálculo del precio final depende de si el cliente es socio o no, y no queremos dos funciones casi idénticas (precio_normal() y precio_socio()) cuando la lógica es la misma con un matiz. Python ofrece un sistema de argumentos extraordinariamente flexible: argumentos por posición y por nombre, valores por defecto, cantidades variables con *args y **kwargs, y desempaquetado en la llamada. En esta lección lo recorremos entero, construimos la función estrella de la tienda — precio_final(base, socio=False, iva=IVA_LIBROS) — y saldamos la deuda que arrastramos desde el módulo 2: entender de verdad cómo funciona zip().
Contenido
- Argumentos posicionales y argumentos con nombre (keyword)
- Valores por defecto:
precio_final()para socios y no socios - La trampa de los valores por defecto mutables
*args: cantidad variable de posicionales**kwargs: cantidad variable de argumentos con nombre- El orden de los parámetros en la definición
- Desempaquetado con
*y**en la llamada - Devolver varios valores (tuplas, presentación breve)
- Deuda saldada:
zip()a fondo
Argumentos posicionales y argumentos con nombre
Hasta ahora hemos emparejado argumentos con parámetros por posición: el primero con el primero, el segundo con el segundo. Python también permite emparejarlos por nombre (argumentos keyword):
def etiquetar(titulo, precio):
print(f"{titulo:<12} {precio:>6.2f} €")
etiquetar("Hamlet", 9.95) # posicionales
etiquetar(titulo="Hamlet", precio=9.95) # con nombre
etiquetar(precio=9.95, titulo="Hamlet") # con nombre: el orden da igual
etiquetar("Hamlet", precio=9.95) # mezcla: posicionales primero
etiquetar(precio=9.95, "Hamlet") # SyntaxError: posicional tras keywordReglas y criterio:
- En una llamada mixta, los posicionales van siempre antes que los keyword.
- Los argumentos con nombre hacen la llamada autoexplicativa:
precio_final(12.50, True)obliga a mirar la definición para saber qué es eseTrue;precio_final(12.50, socio=True)se lee sola. Ya usaste esta idea en el módulo 2 conenumerate(catalogo, start=1)y en el módulo 1 conprint(..., sep=", ", end=""):start,sepyendson argumentos keyword. - Regla práctica: por posición los datos obvios y obligatorios; por nombre las opciones y banderas.
Valores por defecto
Un parámetro puede declarar un valor que se usa si la llamada no lo proporciona. Es el mecanismo perfecto para la función de precios de Papyrus:
IVA_LIBROS = 0.04
DESCUENTO_SOCIO = 0.05
def precio_final(base, socio=False, iva=IVA_LIBROS):
"""Devuelve el precio final: descuento de socio (si procede) y luego IVA."""
if socio:
base = base * (1 - DESCUENTO_SOCIO)
return round(base * (1 + iva), 2)
print(precio_final(12.50)) # 13.0 -> cliente normal
print(precio_final(12.50, socio=True)) # 12.35 -> Luis, socio
print(precio_final(12.50, socio=True, iva=0.21)) # 14.37 -> simulación de otro IVADesglose:
baseno tiene valor por defecto: es obligatorio. Olvidarlo produceTypeError: precio_final() missing 1 required positional argument: 'base'.socio=Falseyiva=IVA_LIBROSson opcionales: la llamada cortaprecio_final(12.50)cubre el caso más frecuente (cliente no socio, IVA de libros) y los casos especiales se piden explícitamente.- El orden de operaciones respeta la convención de la tienda desde el módulo 1: primero el descuento, después el IVA. Los precios de socio del catálogo salen clavados: 12.35, 9.83 y 15.71 € para La Odisea, Hamlet y El Quijote.
Un matiz que sorprende: los valores por defecto se evalúan una sola vez, cuando Python lee el def. iva=IVA_LIBROS toma el valor que IVA_LIBROS tenga en ese momento (0.04) y lo congela; si más tarde reasignaras la constante, el defecto no cambiaría. Con constantes e inmutables esto es inofensivo... pero conduce directamente a la trampa más famosa de Python.
La trampa de los valores por defecto mutables
Como el defecto se evalúa una única vez, usar un objeto mutable (una lista, por ejemplo) como valor por defecto significa que todas las llamadas comparten el mismo objeto:
def apuntar_pedido(titulo, pedidos=[]): # ¡PELIGRO!
pedidos.append(titulo)
return pedidos
print(apuntar_pedido("Fausto")) # ['Fausto']
print(apuntar_pedido("Hamlet")) # ['Fausto', 'Hamlet'] <- ¡¿y ese Fausto?!El segundo pedido "hereda" el primero porque la lista por defecto es la misma en ambas llamadas: se creó al leer el def y ahí sigue, acumulando. El idioma correcto usa None como centinela:
def apuntar_pedido(titulo, pedidos=None):
"""Añade el título a la lista de pedidos (o crea una lista nueva)."""
if pedidos is None:
pedidos = [] # lista NUEVA en cada llamada
pedidos.append(titulo)
return pedidos
print(apuntar_pedido("Fausto")) # ['Fausto']
print(apuntar_pedido("Hamlet")) # ['Hamlet'] <- correcto| Valor por defecto | ¿Seguro? | Ejemplos |
|---|---|---|
| Inmutable | Sí | False, 0, 0.04, "es", None |
| Mutable | No — se comparte entre llamadas | [], {} (usa el patrón None) |
*args: cantidad variable de posicionales
A veces no sabes cuántos valores llegarán. Un parámetro con * delante recoge todos los argumentos posicionales sobrantes:
def total_compra(*importes):
"""Devuelve la suma de todos los importes recibidos."""
total = 0.0
for importe in importes:
total += importe
return round(total, 2)
print(total_compra(12.35)) # 12.35 -> Luis se lleva un libro
print(total_compra(12.35, 9.83, 15.71)) # 37.89 -> se lleva tres
print(total_compra()) # 0.0 -> hoy solo miraba- Dentro de la función,
importeses una tupla con cero o más valores, y se recorre conforcomo cualquier secuencia. - El nombre
argses pura convención (aquíimporteses más expresivo); lo que importa es el*. - Ya conocías esto sin saberlo:
print("a", "b", "c")acepta cualquier cantidad de argumentos gracias a este mecanismo.
**kwargs: cantidad variable de argumentos con nombre
El doble asterisco recoge los argumentos keyword sobrantes en un diccionario (pares nombre-valor; los diccionarios se estudian a fondo en el módulo 4, aquí solo necesitas la idea):
def ficha_libro(titulo, **datos):
"""Imprime una ficha con el título y cualquier dato extra."""
print(f"Ficha de '{titulo}'")
for campo, valor in datos.items():
print(f" {campo}: {valor}")
ficha_libro("Fausto", autor="Goethe", anio=1808, stock=0)Cada libro de Papyrus puede tener datos distintos (editorial, edición, idioma...) sin cambiar la firma de la función. datos.items() recorre los pares nombre-valor; de nuevo, el nombre kwargs (keyword arguments) es solo convención.
El orden de los parámetros en la definición
Cuando combines varias categorías, el orden en el def es fijo:
| Posición | Categoría | Ejemplo |
|---|---|---|
| 1.º | Posicionales obligatorios | base |
| 2.º | Con valor por defecto | socio=False, iva=IVA_LIBROS |
| 3.º | *args |
*importes |
| 4.º | **kwargs |
**datos |
Saltarse el orden (def f(socio=False, base)) es SyntaxError: parameter without a default follows parameter with a default. En la práctica, la mayoría de tus funciones usarán solo las dos primeras categorías — *args/**kwargs son para casos genuinamente variables, no un comodín para no pensar la firma.
Desempaquetado con * y ** en la llamada
Los asteriscos también funcionan en sentido inverso: reparten una secuencia o un diccionario entre los parámetros de la llamada:
datos_fausto = ["Fausto", 21.00]
etiquetar(*datos_fausto) # equivale a etiquetar("Fausto", 21.00)
opciones = {"socio": True, "iva": 0.04}
print(precio_final(21.00, **opciones)) # equivale a socio=True, iva=0.04 -> 20.75*secuenciaen una llamada desempaqueta sus elementos como argumentos posicionales.**diccionariodesempaqueta pares nombre-valor como argumentos keyword (las claves deben coincidir con nombres de parámetros).- Mnemotecnia: en el
def, los asteriscos empaquetan (muchos argumentos → una variable); en la llamada, desempaquetan (una variable → muchos argumentos).
Devolver varios valores (presentación breve)
return puede entregar varios valores separados por comas; técnicamente devuelve una tupla (secuencia inmutable que veremos a fondo en el módulo 4), y lo habitual es desempaquetarla al recibirla — el mismo mecanismo de la asignación múltiple del módulo 1:
def resumen_precio(base, socio=False):
"""Devuelve (precio final, ahorro respecto al precio sin descuento)."""
final = precio_final(base, socio=socio)
sin_descuento = precio_final(base)
return final, round(sin_descuento - final, 2)
precio, ahorro = resumen_precio(12.50, socio=True)
print(f"Luis paga {precio} € y ahorra {ahorro} €") # Luis paga 12.35 € y ahorra 0.65 €Con esto nos basta por ahora; las tuplas tendrán su propia lección (04-02).
Deuda saldada: zip() a fondo
En el módulo 2 usamos zip() de pasada; ha llegado el momento de entenderla. Papyrus guarda sus datos en tres listas paralelas — el elemento i de cada una describe el mismo libro:
zip() recibe varias secuencias y va produciendo tuplas con los elementos emparejados por posición: primero ("La Odisea", 12.50, 4), luego ("Hamlet", 9.95, 6), etc. Es como la cremallera (zipper) de una chaqueta: une un diente de cada lado en cada paso.
for titulo, precio, stock in zip(catalogo, precios, stocks):
print(f"{titulo:<12} {precio:>6.2f} € stock {stock}")Compárala con el patrón de índices que usábamos hasta ahora:
| Con índices | Con zip() |
|
|---|---|---|
| Código | for i, titulo in enumerate(catalogo): ... precios[i] ... stocks[i] |
for titulo, precio, stock in zip(...) |
| Legibilidad | Regular: precios[i] obliga a rastrear i |
Alta: cada variable tiene nombre propio |
| ¿Da el índice? | Sí | No (combínala con enumerate si lo necesitas) |
| ¿Listas de distinta longitud? | IndexError si la que indexa es más larga |
Se detiene en la más corta, en silencio |
Detalles importantes:
zip()es perezosa: no construye una lista de tuplas en memoria, las va entregando a medida que elforlas pide. Si quieres verlas todas de golpe:list(zip(catalogo, precios))→[('La Odisea', 12.5), ('Hamlet', 9.95), ('El Quijote', 15.9)]. Consecuencia práctica: unzip()ya recorrido queda agotado; crea uno nuevo para recorrerlo otra vez.- Que se detenga en la secuencia más corta es cómodo pero peligroso: si a
catalogole añades "Fausto" y olvidas añadir su precio, elforsimplemente ignorará el libro sin avisar. Desde Python 3.10,zip(catalogo, precios, strict=True)lanza un error si las longitudes no coinciden — fíjate:strictes un argumento keyword, como los que acabas de aprender. zip()también combina de maravilla con las funciones de esta lección:
def imprimir_tarifa_socios(titulos, bases):
"""Imprime cada título con su precio de socio."""
for titulo, base in zip(titulos, bases):
print(f"{titulo:<12} {precio_final(base, socio=True):>6.2f} €")
imprimir_tarifa_socios(catalogo, precios)Estas listas paralelas empiezan a ser frágiles (tres listas que mantener sincronizadas a mano); en el módulo 4 conocerás estructuras que agrupan los datos de cada libro en un solo lugar. Hasta entonces, zip() es el pegamento oficial de Papyrus.
Errores Comunes y Consejos
- Posicional después de keyword en la llamada (
precio_final(socio=True, 12.50)):SyntaxError. Primero las posiciones, luego los nombres. - Repetir un argumento (
precio_final(12.50, base=12.50)):TypeError: got multiple values for argument 'base'. El valor llegó por posición y por nombre a la vez. []o{}como valor por defecto: la trampa estrella. UsaNoney crea el objeto dentro. Los linters (como los que sugiere VS Code) lo marcan; hazles caso.- Pasar banderas por posición (
precio_final(12.50, True)): funciona, pero dentro de un mes nadie recordará qué eraTrue. Escribesocio=True. - Confundir
*en eldefcon*en la llamada: en la definición empaqueta; en la llamada desempaqueta. Si dudas, pregúntate de qué lado del paréntesis estás. - Abusar de
*args/**kwargs: una firma explícita documenta y valida sola. Reserva los asteriscos para funciones genuinamente variádicas (comototal_compra). - Olvidar que
zip()calla con longitudes distintas: en datos que deben ir emparejados sí o sí, usastrict=True(Python ≥ 3.10). - Consejo: al diseñar una firma, ordena de más a menos importante y pon defectos a todo lo que tenga un valor "normal". La llamada típica debería ser la más corta.
Ejercicios
Ejercicio 1: el ticket variádico
Escribe ticket(cliente, *importes, socio=False) que imprima una línea Ticket de <cliente>, después cada importe pasado por precio_final(importe, socio=socio) (uno por línea, formato :>6.2f), y al final Total: X.XX € con la suma de los precios finales. Pruébala con ticket("Luis", 12.50, 9.95, 15.90, socio=True) — el total debe ser 37.89 € — y con ticket("Marta", 21.00).
Ejercicio 2: reponer sin sustos
Ana escribió esta función para apuntar reposiciones pendientes y algo va mal:
Ejecuta apuntar_reposicion("Fausto") y luego apuntar_reposicion("Hamlet"), observa el problema, explica por qué ocurre y corrígela con el patrón del centinela None.
Ejercicio 3: tarifa completa con zip()
Con catalogo, precios y stocks (más "Fausto", 21.00, 0 añadidos: usa catalogo + ["Fausto"], etc., para no modificar las originales), imprime con un solo bucle sobre zip() una tabla título | precio normal | precio socio | estado, donde el estado sea agotado si el stock es 0 y disponible en caso contrario. Usa precio_final() para ambos precios.
Soluciones
Ejercicio 1:
def ticket(cliente, *importes, socio=False):
"""Imprime el ticket de una compra con precios finales."""
print(f"Ticket de {cliente}")
total = 0.0
for importe in importes:
final = precio_final(importe, socio=socio)
total += final
print(f"{final:>6.2f} €")
print(f"Total: {round(total, 2)} €")
ticket("Luis", 12.50, 9.95, 15.90, socio=True) # 12.35 / 9.83 / 15.71, Total: 37.89 €
ticket("Marta", 21.00) # 21.84, Total: 21.84 €Como socio va después de *importes, solo puede pasarse por nombre — Python jamás lo confundirá con un importe más. Es un truco de diseño muy usado.
Ejercicio 2: La segunda llamada devuelve ['Fausto', 'Hamlet']: ambas comparten la misma lista por defecto, creada una sola vez al leer el def. Corrección:
def apuntar_reposicion(titulo, pendientes=None):
"""Añade un título a la lista de reposiciones (nueva si no se pasa)."""
if pendientes is None:
pendientes = []
pendientes.append(titulo)
return pendientesAhora cada llamada sin lista recibe una lista recién creada, y quien tenga ya una lista propia puede pasarla: apuntar_reposicion("Hamlet", pendientes=mi_lista).
Ejercicio 3:
titulos = catalogo + ["Fausto"]
bases = precios + [21.00]
unidades = stocks + [0]
print(f"{'Título':<12} {'Normal':>8} {'Socio':>8} Estado")
for titulo, base, stock in zip(titulos, bases, unidades):
estado = "agotado" if stock == 0 else "disponible"
print(f"{titulo:<12} {precio_final(base):>6.2f} € {precio_final(base, socio=True):>6.2f} € {estado}")Cada variable del for recibe su elemento emparejado por zip(); el ternario del módulo 2 resuelve el estado en una línea. Salida: Fausto aparece con 21.84 € / 20.75 € y agotado.
Conclusión
Las funciones de Papyrus ya hablan un lenguaje rico: argumentos posicionales para lo esencial, keyword para las opciones (socio=True), valores por defecto para el caso común — esquivando la trampa de los mutables —, *args/**kwargs para lo variable y desempaquetado con */** en las llamadas. Además, zip() ha dejado de ser magia: es la cremallera que recorre en paralelo las listas de la tienda, y precio_final(base, socio=False, iva=IVA_LIBROS) queda establecida como la función oficial de precios. En la próxima lección conoceremos una forma abreviada de escribir funciones diminutas y anónimas — las lambdas — y las pondremos a trabajar donde de verdad brillan: como criterio de sorted(), min() y max() para, por fin, ordenar el catálogo de Papyrus por precio.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
