🔧 MAD-UdelaR Ejercicios de Funciones

Introduce la contraseña para acceder

❌ Contraseña incorrecta

← Volver al índice

MAD-UdelaR: Ejercicios de Funciones en Python

Daniel Miles Touya

Realiza los Ejercicios en celdas sucesivas de Jupyter Notebook

La solución es una de las posibles (no hay una sola respuesta)

Conceptos Clave sobre Funciones

Funciones: Bloques de código reutilizable que realizan una tarea específica.

Sintaxis: def return parámetros argumentos

Beneficios: Reutilización, organización, legibilidad y facilidad de testing

Estructura: def nombre_funcion(parametros): → return resultado

1
Mi primera función
Fácil
Problema: Crea una función llamada saludar() que no reciba parámetros y que imprima "¡Hola, mundo!" cuando se llame. Luego llama a la función para probarla.

✅ Solución:

def saludar(): print("¡Hola, mundo!") # Llamar a la función saludar() # También podemos llamarla múltiples veces saludar() saludar()

Explicación: Una función se define con def, seguido del nombre y paréntesis. Para ejecutarla, escribimos su nombre seguido de ().

2
Función con parámetro
Fácil
Problema: Crea una función llamada saludar_persona(nombre) que reciba un nombre como parámetro e imprima "¡Hola, [nombre]!". Pruébala con diferentes nombres.

✅ Solución:

def saludar_persona(nombre): print(f"¡Hola, {nombre}!") # Probar con diferentes nombres saludar_persona("Ana") saludar_persona("Carlos") saludar_persona("María") # También funciona con variables mi_nombre = "Pedro" saludar_persona(mi_nombre)

Explicación: Los parámetros van dentro de los paréntesis. Podemos usar el valor del parámetro dentro de la función como si fuera una variable.

3
Función que devuelve un valor
Fácil
Problema: Crea una función llamada sumar(a, b) que reciba dos números y devuelva su suma usando return. Guarda el resultado en una variable e imprímelo.

✅ Solución:

def sumar(a, b): resultado = a + b return resultado # Manera 1: Guardar en variable suma = sumar(5, 3) print(f"5 + 3 = {suma}") # Manera 2: Usar directamente print(f"10 + 7 = {sumar(10, 7)}") # Manera 3: Usar con variables num1 = 15 num2 = 25 total = sumar(num1, num2) print(f"{num1} + {num2} = {total}")

Explicación: return devuelve un valor desde la función. Podemos guardar ese valor en una variable o usarlo directamente.

4
Función con múltiples parámetros
Fácil
Problema: Crea una función llamada presentar_persona(nombre, edad, ciudad) que reciba tres parámetros y devuelva un texto como: "Me llamo [nombre], tengo [edad] años y vivo en [ciudad]".

✅ Solución:

def presentar_persona(nombre, edad, ciudad): presentacion = f"Me llamo {nombre}, tengo {edad} años y vivo en {ciudad}" return presentacion # Probar la función mensaje1 = presentar_persona("Ana", 25, "Madrid") print(mensaje1) mensaje2 = presentar_persona("Carlos", 30, "Barcelona") print(mensaje2) # También podemos imprimir directamente print(presentar_persona("María", 22, "Sevilla")) # Usar con variables mi_nombre = "Pedro" mi_edad = 28 mi_ciudad = "Valencia" mi_presentacion = presentar_persona(mi_nombre, mi_edad, mi_ciudad) print(mi_presentacion)

Explicación: Las funciones pueden recibir múltiples parámetros separados por comas. El orden importa al llamar la función.

5
Función con parámetro por defecto
Fácil
Problema: Crea una función llamada calcular_area_rectangulo(ancho, alto=1) que calcule el área de un rectángulo. Si no se proporciona la altura, debe usar 1 por defecto (será un cuadrado).

✅ Solución:

def calcular_area_rectangulo(ancho, alto=1): area = ancho * alto return area # Usando con ambos parámetros (rectángulo) area_rectangulo = calcular_area_rectangulo(5, 3) print(f"Área del rectángulo 5x3: {area_rectangulo}") # Usando solo el ancho (cuadrado, alto=1 por defecto) area_cuadrado = calcular_area_rectangulo(4) print(f"Área del cuadrado 4x1: {area_cuadrado}") # Más ejemplos print(f"Rectángulo 10x2: {calcular_area_rectangulo(10, 2)}") print(f"Cuadrado 7x1: {calcular_area_rectangulo(7)}") # También podemos especificar el parámetro por nombre area_especifica = calcular_area_rectangulo(ancho=6, alto=4) print(f"Rectángulo 6x4: {area_especifica}") # O cambiar el orden si usamos nombres area_con_nombres = calcular_area_rectangulo(alto=8, ancho=3) print(f"Rectángulo 3x8: {area_con_nombres}")

Explicación: Los parámetros pueden tener valores por defecto usando =. Si no pasamos ese argumento, usa el valor por defecto.

6
Keyword Arguments (argumentos con nombre)
Fácil
Problema: Crea una función llamada crear_perfil(nombre, edad, profesion="Estudiante", activo=True) que muestre información de un perfil de usuario. Prueba llamándola de diferentes formas: con argumentos posicionales, con keywords, y mezclando ambos.

✅ Solución:

def crear_perfil(nombre, edad, profesion="Estudiante", activo=True): estado = "Activo" if activo else "Inactivo" perfil = f"Nombre: {nombre}\nEdad: {edad}\nProfesión: {profesion}\nEstado: {estado}" return perfil # Forma 1: Solo argumentos posicionales (orden importa) print("=== ARGUMENTOS POSICIONALES ===") perfil1 = crear_perfil("Ana", 22) print(perfil1) print() perfil2 = crear_perfil("Carlos", 30, "Ingeniero", False) print(perfil2) print() # Forma 2: Solo keyword arguments (orden NO importa) print("=== KEYWORD ARGUMENTS ===") perfil3 = crear_perfil(nombre="María", edad=25, profesion="Doctora") print(perfil3) print() perfil4 = crear_perfil(edad=28, nombre="Luis", activo=False, profesion="Profesor") print(perfil4) print() # Forma 3: Mezclando posicionales y keywords print("=== MEZCLANDO AMBOS ===") perfil5 = crear_perfil("Sara", 24, profesion="Artista") print(perfil5) print() perfil6 = crear_perfil("Pedro", 35, activo=False) print(perfil6) print() # Forma 4: Especificando solo algunos keywords print("=== CASOS ESPECIALES ===") perfil7 = crear_perfil("Elena", 29, profesion="Científica", activo=True) print(perfil7) print() # Los posicionales DEBEN ir antes que los keywords perfil8 = crear_perfil("Roberto", edad=33, profesion="Abogado") print(perfil8)

Explicación: Los keyword arguments permiten llamar funciones especificando el nombre del parámetro. Esto hace el código más legible y permite cambiar el orden de los argumentos.

7
Introducción a *args - Suma variable
Fácil
Problema: Crea una función llamada sumar_todos(*numeros) que pueda recibir cualquier cantidad de números y devuelva su suma. Pruébala con diferentes cantidades de argumentos.

✅ Solución:

def sumar_todos(*numeros): """Suma cualquier cantidad de números""" total = 0 for numero in numeros: total += numero return total # Probando con diferentes cantidades de argumentos print("=== PROBANDO SUMAR_TODOS ===") # Con 2 números resultado1 = sumar_todos(5, 3) print(f"sumar_todos(5, 3) = {resultado1}") # Con 3 números resultado2 = sumar_todos(10, 20, 30) print(f"sumar_todos(10, 20, 30) = {resultado2}") # Con muchos números resultado3 = sumar_todos(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) print(f"sumar_todos(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) = {resultado3}") # Con un solo número resultado4 = sumar_todos(42) print(f"sumar_todos(42) = {resultado4}") # Sin argumentos resultado5 = sumar_todos() print(f"sumar_todos() = {resultado5}") # Usando la función sum() de Python (alternativa) def sumar_todos_v2(*numeros): return sum(numeros) print(f"\nVersión con sum(): {sumar_todos_v2(1, 2, 3, 4, 5)}")

Explicación: *args permite que una función reciba cualquier cantidad de argumentos posicionales. Dentro de la función, args es una tupla con todos los valores.

8
*args con parámetros fijos
Fácil
Problema: Crea una función llamada saludar_grupo(saludo, *nombres) que reciba un saludo fijo y luego cualquier cantidad de nombres. Debe imprimir el saludo personalizado para cada persona.

✅ Solución:

def saludar_grupo(saludo, *nombres): """Saluda a un grupo de personas con el mismo mensaje""" if not nombres: print("No hay nadie a quien saludar 🤷‍♂️") return print(f"=== {saludo.upper()} ===") for i, nombre in enumerate(nombres, 1): print(f"{i}. {saludo}, {nombre}!") # Probando la función print("Caso 1: Saludo de buenos días") saludar_grupo("Buenos días", "Ana", "Carlos", "María") print("\nCaso 2: Saludo informal") saludar_grupo("¡Hola", "Pedro", "Luis") print("\nCaso 3: Saludo formal") saludar_grupo("Bienvenido", "Dr. García", "Sra. López", "Prof. Martín", "Ing. Torres") print("\nCaso 4: Solo una persona") saludar_grupo("¡Feliz cumpleaños", "Sofía") print("\nCaso 5: Sin nombres") saludar_grupo("Hola") # Versión que devuelve lista de saludos def crear_saludos(saludo, *nombres): """Crea una lista de saludos personalizados""" saludos = [] for nombre in nombres: saludos.append(f"{saludo}, {nombre}!") return saludos print("\n=== VERSIÓN QUE DEVUELVE LISTA ===") lista_saludos = crear_saludos("¡Buen día", "Elena", "Roberto", "Carmen") for saludo_personal in lista_saludos: print(saludo_personal)

Explicación: Podemos combinar parámetros normales con *args. Los parámetros fijos van primero, seguidos de *args que captura el resto.

9
Calculadora con operaciones múltiples
Medio
Problema: Crea una función llamada calculadora(operacion, *numeros) que pueda realizar diferentes operaciones matemáticas (suma, multiplicación, máximo, mínimo) sobre cualquier cantidad de números.

✅ Solución:

def calculadora(operacion, *numeros): """Calculadora que opera sobre múltiples números""" if not numeros: return "Error: No se proporcionaron números" match operacion.lower(): case "suma" | "sumar" | "+": resultado = sum(numeros) operacion_str = " + ".join(map(str, numeros)) case "multiplicacion" | "multiplicar" | "*": resultado = 1 for num in numeros: resultado *= num operacion_str = " × ".join(map(str, numeros)) case "maximo" | "max": resultado = max(numeros) operacion_str = f"máximo de {numeros}" case "minimo" | "min": resultado = min(numeros) operacion_str = f"mínimo de {numeros}" case "promedio" | "media": resultado = sum(numeros) / len(numeros) operacion_str = f"promedio de {numeros}" case _: return f"Error: Operación '{operacion}' no reconocida" return f"{operacion_str} = {resultado}" # Probando la calculadora print("=== CALCULADORA CON *ARGS ===") # Suma print(calculadora("suma", 10, 20, 30, 40)) print(calculadora("+", 5, 15, 25)) # Multiplicación print(calculadora("multiplicacion", 2, 3, 4, 5)) print(calculadora("*", 7, 8, 9)) # Máximo y mínimo print(calculadora("maximo", 45, 12, 78, 23, 56)) print(calculadora("minimo", 45, 12, 78, 23, 56)) # Promedio print(calculadora("promedio", 10, 20, 30, 40, 50)) # Casos especiales print(calculadora("suma", 42)) # Un solo número print(calculadora("division", 10, 5)) # Operación no válida print(calculadora("suma")) # Sin números # Función avanzada con más opciones def calculadora_avanzada(operacion, *numeros, precision=2): """Calculadora con precisión configurable""" if not numeros: return "Error: No se proporcionaron números" match operacion.lower(): case "suma": resultado = sum(numeros) case "multiplicacion": resultado = 1 for num in numeros: resultado *= num case "promedio": resultado = sum(numeros) / len(numeros) case "varianza": media = sum(numeros) / len(numeros) resultado = sum((x - media) ** 2 for x in numeros) / len(numeros) case _: return f"Operación '{operacion}' no disponible" return round(resultado, precision) print(f"\n=== CALCULADORA AVANZADA ===") print(f"Promedio: {calculadora_avanzada('promedio', 1, 2, 3, 4, 5)}") print(f"Varianza: {calculadora_avanzada('varianza', 10, 12, 23, 23, 16, 23, 21, 16)}")

Explicación: Podemos usar *args para crear funciones flexibles que manejen diferentes cantidades de datos. Útil para operaciones matemáticas que pueden aplicarse a conjuntos variables.

10
Analizador de datos con *args
Difícil
Problema: Crea un sistema de análisis de datos con una función analizar_ventas(*ventas, mostrar_detalle=True, currency="€") que pueda analizar cualquier cantidad de datos de ventas y generar estadísticas completas con visualización opcional.

✅ Solución:

def analizar_ventas(*ventas, mostrar_detalle=True, currency="€"): """Analiza datos de ventas y genera estadísticas completas""" if not ventas: return "❌ No se proporcionaron datos de ventas" # Convertir a lista para facilitar operaciones datos = list(ventas) n = len(datos) # Estadísticas básicas total = sum(datos) promedio = total / n maximo = max(datos) minimo = min(datos) # Estadísticas avanzadas datos_ordenados = sorted(datos) # Mediana if n % 2 == 0: mediana = (datos_ordenados[n//2 - 1] + datos_ordenados[n//2]) / 2 else: mediana = datos_ordenados[n//2] # Cuartiles q1_pos = n // 4 q3_pos = 3 * n // 4 q1 = datos_ordenados[q1_pos] if q1_pos < n else datos_ordenados[-1] q3 = datos_ordenados[q3_pos] if q3_pos < n else datos_ordenados[-1] # Rango intercuartílico iqr = q3 - q1 # Desviación estándar varianza = sum((x - promedio) ** 2 for x in datos) / n desviacion = varianza ** 0.5 # Análisis de tendencias ventas_altas = [v for v in datos if v > promedio * 1.2] ventas_bajas = [v for v in datos if v < promedio * 0.8] # Crear reporte print("=" * 50) print("📊 ANÁLISIS DE VENTAS") print("=" * 50) # Resumen ejecutivo print(f"🎯 RESUMEN EJECUTIVO:") print(f" Total vendido: {total:,.2f} {currency}") print(f" Promedio: {promedio:,.2f} {currency}") print(f" Mejor venta: {maximo:,.2f} {currency}") print(f" Peor venta: {minimo:,.2f} {currency}") print(f" Número de ventas: {n}") print(f"\n📈 ESTADÍSTICAS AVANZADAS:") print(f" Mediana: {mediana:,.2f} {currency}") print(f" Q1 (25%): {q1:,.2f} {currency}") print(f" Q3 (75%): {q3:,.2f} {currency}") print(f" Rango IQR: {iqr:,.2f} {currency}") print(f" Desviación estándar: {desviacion:,.2f} {currency}") print(f"\n🔍 ANÁLISIS DE RENDIMIENTO:") print(f" Ventas superiores (+20%): {len(ventas_altas)} ({len(ventas_altas)/n*100:.1f}%)") print(f" Ventas inferiores (-20%): {len(ventas_bajas)} ({len(ventas_bajas)/n*100:.1f}%)") if mostrar_detalle: print(f"\n📋 DATOS DETALLADOS:") print(f" Datos originales: {datos}") print(f" Datos ordenados: {datos_ordenados}") # Histograma simple print(f"\n📊 DISTRIBUCIÓN (Histograma simple):") rangos = crear_histograma(datos, bins=5) for rango, freq in rangos: barra = "█" * freq print(f" {rango}: {barra} ({freq})") # Recomendaciones print(f"\n💡 RECOMENDACIONES:") if desviacion > promedio * 0.3: print(" ⚠️ Alta variabilidad en ventas - revisar estrategia") if len(ventas_bajas) > n * 0.3: print(" 📉 Muchas ventas por debajo del promedio - analizar causas") if maximo > promedio * 2: print(" 🎯 Hay ventas excepcionales - identificar factores de éxito") return { 'total': total, 'promedio': promedio, 'mediana': mediana, 'desviacion': desviacion, 'max': maximo, 'min': minimo } def crear_histograma(datos, bins=5): """Crea un histograma simple de los datos""" minimo, maximo = min(datos), max(datos) rango = maximo - minimo tamaño_bin = rango / bins histograma = [] for i in range(bins): inicio = minimo + i * tamaño_bin fin = inicio + tamaño_bin if i == bins - 1: # Último bin incluye el máximo count = sum(1 for x in datos if inicio <= x <= fin) else: count = sum(1 for x in datos if inicio <= x < fin) histograma.append((f"{inicio:.0f}-{fin:.0f}", count)) return histograma def comparar_periodos(*periodos, nombres=None): """Compara múltiples períodos de ventas""" if nombres is None: nombres = [f"Período {i+1}" for i in range(len(periodos))] print("=" * 60) print("📊 COMPARACIÓN DE PERÍODOS") print("=" * 60) resultados = [] for i, periodo in enumerate(periodos): if isinstance(periodo, (list, tuple)): datos = list(periodo) else: datos = [periodo] total = sum(datos) promedio = total / len(datos) resultados.append((nombres[i], total, promedio, len(datos))) # Mostrar comparación print(f"{'Período':<15} {'Total':<12} {'Promedio':<12} {'N° Ventas':<10} {'Crecimiento':<12}") print("-" * 65) for i, (nombre, total, promedio, n_ventas) in enumerate(resultados): if i == 0: crecimiento = "Base" else: crecimiento_pct = ((total - resultados[0][1]) / resultados[0][1]) * 100 crecimiento = f"{crecimiento_pct:+.1f}%" print(f"{nombre:<15} {total:<12.2f} {promedio:<12.2f} {n_ventas:<10} {crecimiento:<12}") # ===== CASOS DE PRUEBA ===== print("=== ANÁLISIS DE VENTAS MENSUALES ===") # Datos de ventas de enero ventas_enero = analizar_ventas(1200, 1500, 800, 2200, 1800, 950, 1600, mostrar_detalle=True, currency="€") print("\n" + "="*60) # Datos de ventas trimestrales print("=== ANÁLISIS TRIMESTRAL ===") ventas_q1 = analizar_ventas(15000, 18000, 22000, 16500, 19000, 17500, 20000, 18500, 21000, 16000, 19500, 17800, mostrar_detalle=False, currency="$") print("\n" + "="*60) # Comparación de períodos print("=== COMPARACIÓN DE TRIMESTRES ===") q1_datos = [15000, 18000, 22000, 16500, 19000, 17500] q2_datos = [21000, 23000, 19000, 25000, 22000, 20000] q3_datos = [18000, 20000, 17000, 19500, 21500, 23000] comparar_periodos(q1_datos, q2_datos, q3_datos, nombres=["Q1 2024", "Q2 2024", "Q3 2024"]) # Función para análisis de productos específicos def analizar_producto(nombre_producto, *ventas_mensuales, objetivo=None): """Analiza las ventas de un producto específico""" print(f"\n📦 ANÁLISIS DE PRODUCTO: {nombre_producto}") print("-" * 40) total_anual = sum(ventas_mensuales) promedio_mensual = total_anual / len(ventas_mensuales) print(f"Total anual: {total_anual:,.2f} €") print(f"Promedio mensual: {promedio_mensual:,.2f} €") if objetivo: cumplimiento = (total_anual / objetivo) * 100 print(f"Objetivo anual: {objetivo:,.2f} €") print(f"Cumplimiento: {cumplimiento:.1f}%") if cumplimiento >= 100: print("✅ Objetivo alcanzado") else: faltante = objetivo - total_anual print(f"❌ Falta: {faltante:,.2f} € ({100-cumplimiento:.1f}%)") print("\n" + "="*60) print("=== ANÁLISIS POR PRODUCTOS ===") analizar_producto("Laptop Pro", 5000, 6200, 4800, 7100, 5900, 6400, 5200, 6800, 5600, 7200, 6100, 5500, objetivo=70000) analizar_producto("Mouse Inalámbrico", 1200, 1400, 1100, 1600, 1350, 1250, 1450, 1300, 1500, 1400, 1200, 1300, objetivo=16000)

Explicación: Este ejercicio combina *args con análisis estadístico real: cálculo de mediana, cuartiles, desviación estándar, histogramas y comparaciones. Demuestra cómo *args es fundamental en ciencia de datos para funciones flexibles.

11
Tipos de argumentos - Sintaxis y orden
Fácil
Problema: Crea una función ejemplo_argumentos(pos1, pos2, default1="valor", default2="otro", *args, kw_only, **kwargs) que demuestre todos los tipos de argumentos. Explora qué pasa si cambias el orden o llamas la función incorrectamente.

✅ Solución:

def ejemplo_argumentos(pos1, pos2, default1="valor", default2="otro", *args, kw_only, **kwargs): """Función que demuestra todos los tipos de argumentos""" print("=== ANÁLISIS DE ARGUMENTOS ===") print(f"📍 Posicionales obligatorios:") print(f" pos1 = {pos1} (tipo: {type(pos1).__name__})") print(f" pos2 = {pos2} (tipo: {type(pos2).__name__})") print(f"\n🔧 Con valores por defecto:") print(f" default1 = {default1}") print(f" default2 = {default2}") print(f"\n📦 Argumentos variables (*args):") if args: print(f" args = {args} (tipo: {type(args).__name__})") print(f" Cantidad de argumentos extra: {len(args)}") for i, arg in enumerate(args): print(f" args[{i}] = {arg}") else: print(f" args = () (vacío)") print(f"\n🔑 Keyword-only (después de *args):") print(f" kw_only = {kw_only}") print(f"\n🗂️ Argumentos keyword variables (**kwargs):") if kwargs: print(f" kwargs = {kwargs} (tipo: {type(kwargs).__name__})") print(f" Cantidad de kwargs: {len(kwargs)}") for key, value in kwargs.items(): print(f" {key} = {value}") else: print(f" kwargs = {{}} (vacío)") # ===== DEMOSTRACIONES ===== print("=== TIPOS DE ARGUMENTOS EN PYTHON ===") print("1. Llamada mínima (solo obligatorios + kw_only):") try: ejemplo_argumentos("A", "B", kw_only="requerido") except Exception as e: print(f"❌ Error: {e}") print("\n" + "="*60) print("2. Llamada con algunos defaults:") ejemplo_argumentos("A", "B", "nuevo_default1", kw_only="requerido") print("\n" + "="*60) print("3. Llamada con todos los defaults:") ejemplo_argumentos("A", "B", "nuevo_default1", "nuevo_default2", kw_only="requerido") print("\n" + "="*60) print("4. Llamada con argumentos extra (*args):") ejemplo_argumentos("A", "B", "nuevo_default1", "nuevo_default2", "extra1", "extra2", "extra3", kw_only="requerido") print("\n" + "="*60) print("5. Llamada con argumentos keyword (**kwargs):") ejemplo_argumentos("A", "B", kw_only="requerido", extra_kw1="valor1", extra_kw2="valor2", numero=42) print("\n" + "="*60) print("6. Llamada completa con todos los tipos:") ejemplo_argumentos("A", "B", "custom1", "custom2", "extra1", "extra2", kw_only="requerido", ciudad="Madrid", edad=25, activo=True) # ===== CASOS DE ERROR COMUNES ===== print("\n" + "="*60) print("=== CASOS DE ERROR COMUNES ===") print("\n1. ❌ Falta argumento posicional obligatorio:") try: ejemplo_argumentos("solo_uno", kw_only="requerido") except TypeError as e: print(f"Error: {e}") print("\n2. ❌ Falta keyword-only argument:") try: ejemplo_argumentos("A", "B") except TypeError as e: print(f"Error: {e}") print("\n3. ❌ Intentar pasar kw_only como posicional:") try: ejemplo_argumentos("A", "B", "default1", "default2", "extra", "valor_kw_only") except TypeError as e: print(f"Error: {e}") print("\n4. ❌ Demasiados argumentos posicionales:") # Esta en realidad no da error porque *args captura extras ejemplo_argumentos("A", "B", "c", "d", "e", "f", "g", "h", kw_only="requerido") print("^ Nota: No da error porque *args captura argumentos extra") # ===== ORDEN CORRECTO DE PARÁMETROS ===== print("\n" + "="*60) print("=== ORDEN CORRECTO DE PARÁMETROS ===") print("✅ Orden correcto en definición de función:") print("def func(pos_required, pos_required2, pos_default=val, *args, kw_only, **kwargs):") print(" 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣") print() print("1️⃣ Posicionales obligatorios") print("2️⃣ Más posicionales obligatorios") print("3️⃣ Posicionales con valores por defecto") print("4️⃣ *args (captura argumentos posicionales extra)") print("5️⃣ Keyword-only arguments (después de *args)") print("6️⃣ **kwargs (captura argumentos keyword extra)") # Función que demuestra orden incorrecto print("\n❌ Ejemplos de orden INCORRECTO:") print("\n# Esto NO funciona:") print("# def func_mal(*args, pos_required): # Error: pos después de *args") print("\n# Esto tampoco:") print("# def func_mal(default='val', pos_required): # Error: pos después de default") print("\n# Ni esto:") print("# def func_mal(**kwargs, *args): # Error: *args después de **kwargs") # ===== EJEMPLO PRÁCTICO: FUNCIÓN DE CONFIGURACIÓN ===== def configurar_app(nombre, version, debug=False, *modulos, base_datos, **configuraciones): """Ejemplo práctico de función con todos los tipos de argumentos""" print(f"🚀 CONFIGURANDO APLICACIÓN") print(f"📱 Nombre: {nombre} v{version}") print(f"🐛 Debug: {'Activado' if debug else 'Desactivado'}") print(f"🗄️ Base de datos: {base_datos}") if modulos: print(f"📦 Módulos adicionales: {', '.join(modulos)}") if configuraciones: print(f"⚙️ Configuraciones extra:") for key, value in configuraciones.items(): print(f" {key}: {value}") print("\n" + "="*60) print("=== EJEMPLO PRÁCTICO ===") configurar_app( "MiApp", # posicional obligatorio "1.0.0", # posicional obligatorio True, # posicional con default "auth", "payments", "logs", # *args (módulos extra) base_datos="PostgreSQL", # keyword-only puerto=5432, # **kwargs ssl=True, # **kwargs timeout=30 # **kwargs ) # ===== RESUMEN DE SINTAXIS ===== print("\n" + "="*60) print("=== RESUMEN DE SINTAXIS ===") print(""" 📋 DEFINICIÓN DE FUNCIÓN: def funcion(pos, pos_def='val', *args, kw_only, **kwargs): 📞 LLAMADA DE FUNCIÓN: # Posicionales funcion(val1, val2, val3, val4, kw_only=valor) # Keywords funcion(pos=val1, pos_def=val2, kw_only=valor) # Mixto funcion(val1, pos_def=val2, kw_only=valor, extra=val3) 🔑 REGLAS IMPORTANTES: 1. Posicionales van antes que keywords en llamadas 2. *args va después de parámetros normales en definición 3. keyword-only van después de *args 4. **kwargs siempre va al final 5. kw_only DEBE pasarse como keyword, no posicional """) # Función para verificar sintaxis def verificar_sintaxis(): """Demuestra qué está permitido y qué no""" print("✅ PERMITIDO:") print(" func(1, 2, c=3)") print(" func(a=1, b=2, c=3)") print(" func(1, b=2, c=3)") print("\n❌ NO PERMITIDO:") print(" func(a=1, 2) # keyword antes que posicional") print(" func(1, 2, 3, a=1) # si solo acepta 2 posicionales") verificar_sintaxis()

Explicación: Los argumentos en Python tienen un orden específico: posicionales → defaults → *args → keyword-only → **kwargs. El orden en las llamadas también importa: posicionales antes que keywords.

12
Packing vs Unpacking - Definición de función
Medio
Problema: Explora la diferencia entre "packing" (empaquetar) argumentos en la definición de una función usando *args y **kwargs vs "unpacking" (desempaquetar) al llamar una función. Crea ejemplos que muestren ambos conceptos claramente.

✅ Solución:

# ===== PACKING (EMPAQUETADO) EN DEFINICIÓN ===== def ejemplo_packing(*args, **kwargs): """ PACKING: Los * y ** en la DEFINICIÓN 'empaquetan' argumentos *args empaqueta argumentos posicionales en una tupla **kwargs empaqueta argumentos keyword en un diccionario """ print("📦 PACKING EN ACCIÓN:") print(f" *args empaquetó en tupla: {args} (tipo: {type(args).__name__})") print(f" **kwargs empaquetó en dict: {kwargs} (tipo: {type(kwargs).__name__})") return args, kwargs print("=== PACKING (EMPAQUETADO) ===") print("En la DEFINICIÓN, * y ** EMPAQUETAN argumentos\n") print("Llamada: ejemplo_packing(1, 2, 3, nombre='Ana', edad=25)") resultado = ejemplo_packing(1, 2, 3, nombre='Ana', edad=25) print(f"\nResultado devuelto: {resultado}") print(f"args es una tupla: {type(resultado[0])}") print(f"kwargs es un dict: {type(resultado[1])}") # ===== UNPACKING (DESEMPAQUETADO) EN LLAMADAS ===== def funcion_normal(a, b, c, nombre, edad, ciudad="Madrid"): """Función normal que espera argumentos específicos""" print(f"📋 ARGUMENTOS RECIBIDOS:") print(f" a={a}, b={b}, c={c}") print(f" nombre={nombre}, edad={edad}, ciudad={ciudad}") print("\n" + "="*60) print("=== UNPACKING (DESEMPAQUETADO) ===") print("En la LLAMADA, * y ** DESEMPAQUETAN colecciones\n") # Datos para desempaquetar numeros = [1, 2, 3] datos_persona = {'nombre': 'Carlos', 'edad': 30, 'ciudad': 'Barcelona'} print("1. Sin unpacking (ERROR):") try: funcion_normal(numeros, datos_persona) except TypeError as e: print(f" ❌ Error: {e}") print("\n2. Con unpacking (CORRECTO):") print(" Llamada: funcion_normal(*numeros, **datos_persona)") funcion_normal(*numeros, **datos_persona) # ===== DEMOSTRACIÓN VISUAL DEL PROCESO ===== print("\n" + "="*60) print("=== DEMOSTRACIÓN VISUAL ===") print("📦 PACKING (en definición):") print(" def func(*args, **kwargs):") print(" args1, args2, args3 ──────┐") print(" ▼") print(" args = (args1, args2, args3)") print() print(" kw1=val1, kw2=val2 ──────┐") print(" ▼") print(" kwargs = {'kw1': val1, 'kw2': val2}") print("\n📤 UNPACKING (en llamada):") print(" lista = [1, 2, 3]") print(" func(*lista) # Equivalent to func(1, 2, 3)") print(" lista ──────┐") print(" ▼") print(" 1, 2, 3 (argumentos separados)") print() print(" dict = {'a': 1, 'b': 2}") print(" func(**dict) # Equivalent to func(a=1, b=2)") print(" dict ──────┐") print(" ▼") print(" a=1, b=2 (argumentos keyword separados)") # ===== EJEMPLOS PRÁCTICOS DE UNPACKING ===== print("\n" + "="*60) print("=== EJEMPLOS PRÁCTICOS DE UNPACKING ===") def calcular_promedio(*numeros): """Función que usa packing para recibir números variables""" if not numeros: return 0 return sum(numeros) / len(numeros) def mostrar_info(nombre, apellido, **info_extra): """Función que usa packing para info extra""" print(f"👤 {nombre} {apellido}") for clave, valor in info_extra.items(): print(f" {clave}: {valor}") print("1. Unpacking con listas:") notas = [85, 90, 78, 92, 88] print(f" notas = {notas}") print(f" calcular_promedio(*notas) = {calcular_promedio(*notas)}") print("\n2. Unpacking con tuplas:") coordenadas = (10, 20, 30) print(f" coordenadas = {coordenadas}") print(f" calcular_promedio(*coordenadas) = {calcular_promedio(*coordenadas)}") print("\n3. Unpacking con diccionarios:") persona = { 'nombre': 'María', 'apellido': 'García', 'edad': 28, 'ciudad': 'Madrid', 'profesion': 'Ingeniera' } print(f" persona = {persona}") print(" mostrar_info(**persona):") mostrar_info(**persona) # ===== CASOS MIXTOS: PACKING Y UNPACKING JUNTOS ===== print("\n" + "="*60) print("=== PACKING Y UNPACKING COMBINADOS ===") def middleware(*args, **kwargs): """Función que recibe cualquier argumento y los pasa a otra función""" print("🔄 MIDDLEWARE: Recibiendo argumentos (packing)") print(f" args recibidos: {args}") print(f" kwargs recibidos: {kwargs}") print("\n🔄 MIDDLEWARE: Pasando argumentos (unpacking)") return funcion_normal(*args, **kwargs) print("Llamada: middleware(1, 2, 3, nombre='Elena', edad=32)") middleware(1, 2, 3, nombre='Elena', edad=32) # ===== ERRORES COMUNES ===== print("\n" + "="*60) print("=== ERRORES COMUNES ===") print("1. ❌ Confundir packing con unpacking:") print(" # WRONG: intentar usar lista directamente") print(" # func([1, 2, 3]) # Pasa la lista como UN argumento") print(" # CORRECT: desempaquetar la lista") print(" # func(*[1, 2, 3]) # Pasa 1, 2, 3 como argumentos separados") print("\n2. ❌ Intentar desempaquetar tipos incompatibles:") try: print(" Intentando: calcular_promedio(*42)") calcular_promedio(*42) except TypeError as e: print(f" Error: {e}") print("\n3. ❌ Desempaquetar diccionario con claves incorrectas:") try: dict_malo = {'x': 1, 'y': 2, 'z': 3} print(f" Intentando: funcion_normal(**{dict_malo})") funcion_normal(**dict_malo) except TypeError as e: print(f" Error: {e}") # ===== RESUMEN CONCEPTUAL ===== print("\n" + "="*60) print("=== RESUMEN CONCEPTUAL ===") print(""" 📦 PACKING (Empaquetado) - EN DEFINICIÓN: def func(*args, **kwargs): • *args empaqueta argumentos posicionales en tupla • **kwargs empaqueta argumentos keyword en diccionario • Ocurre cuando DEFINES la función 📤 UNPACKING (Desempaquetado) - EN LLAMADA: func(*lista, **diccionario) • *lista desempaqueta elementos como argumentos separados • **dict desempaqueta pares clave-valor como argumentos keyword • Ocurre cuando LLAMAS la función 🔄 FLUJO COMPLETO: lista = [1, 2, 3] # Datos originales func(*lista) # UNPACKING: 1, 2, 3 def func(*args): # PACKING: args = (1, 2, 3) print(args) # Resultado: (1, 2, 3) 💡 REGLA MNEMÓNICA: • DEFINICIÓN: * y ** RECOGEN (pack) • LLAMADA: * y ** DISPERSAN (unpack) """) # Demostración final def demo_final(*elementos, **propiedades): """Demo que muestra el ciclo completo""" print(f"🔄 CICLO COMPLETO DEMOSTRADO:") print(f" Elementos empaquetados: {elementos}") print(f" Propiedades empaquetadas: {propiedades}") # Preparar datos mis_elementos = ["agua", "fuego", "tierra", "aire"] mis_propiedades = {"color": "azul", "tamaño": "grande", "activo": True} print(f"\nDatos originales:") print(f" mis_elementos = {mis_elementos}") print(f" mis_propiedades = {mis_propiedades}") print(f"\nDesempaquetando en llamada:") print(f" demo_final(*mis_elementos, **mis_propiedades)") demo_final(*mis_elementos, **mis_propiedades)

Explicación: PACKING ocurre en la definición (*args, **kwargs recogen argumentos). UNPACKING ocurre en la llamada (*lista, **dict dispersan elementos). Son operaciones inversas.

13
Scope y mutabilidad en argumentos
Medio
Problema: Explora cómo se comportan los argumentos cuando son mutables (listas, diccionarios) vs inmutables (números, strings). Demuestra la diferencia entre modificar argumentos dentro de una función y el concepto de "default mutable arguments".

✅ Solución:

# ===== ARGUMENTOS INMUTABLES ===== def modificar_inmutable(numero, texto): """Intentamos modificar argumentos inmutables""" print(f"🔒 INMUTABLES - Dentro de la función:") print(f" numero recibido: {numero}") print(f" texto recibido: '{texto}'") # Intentamos "modificar" numero = numero + 10 texto = texto + " modificado" print(f" número después de +10: {numero}") print(f" texto después de concatenar: '{texto}'") return numero, texto print("=== ARGUMENTOS INMUTABLES ===") print("Los inmutables (int, str, tuple) NO se modifican fuera de la función\n") mi_numero = 5 mi_texto = "original" print(f"ANTES de la función:") print(f" mi_numero = {mi_numero}") print(f" mi_texto = '{mi_texto}'") resultado = modificar_inmutable(mi_numero, mi_texto) print(f"\nDESPUÉS de la función:") print(f" mi_numero = {mi_numero} (sin cambios)") print(f" mi_texto = '{mi_texto}' (sin cambios)") print(f" valores devueltos = {resultado}") # ===== ARGUMENTOS MUTABLES ===== def modificar_mutable(lista, diccionario): """Modificamos argumentos mutables""" print(f"🔓 MUTABLES - Dentro de la función:") print(f" lista recibida: {lista}") print(f" diccionario recibido: {diccionario}") # Modificamos directamente lista.append("nuevo_elemento") diccionario["nueva_clave"] = "nuevo_valor" print(f" lista después de append: {lista}") print(f" dict después de agregar clave: {diccionario}") print("\n" + "="*60) print("=== ARGUMENTOS MUTABLES ===") print("Los mutables (list, dict, set) SÍ se modifican fuera de la función\n") mi_lista = [1, 2, 3] mi_dict = {"clave": "valor"} print(f"ANTES de la función:") print(f" mi_lista = {mi_lista}") print(f" mi_dict = {mi_dict}") modificar_mutable(mi_lista, mi_dict) print(f"\nDESPUÉS de la función:") print(f" mi_lista = {mi_lista} (¡CAMBIÓ!)") print(f" mi_dict = {mi_dict} (¡CAMBIÓ!)") # ===== REASIGNACIÓN VS MODIFICACIÓN ===== def reasignar_vs_modificar(lista1, lista2): """Demuestra la diferencia entre reasignar y modificar""" print(f"📝 REASIGNACIÓN VS MODIFICACIÓN:") print(f" lista1 original: {lista1}") print(f" lista2 original: {lista2}") # REASIGNACIÓN - crea nueva referencia local lista1 = [999, 888, 777] print(f" lista1 después de reasignar: {lista1}") # MODIFICACIÓN - cambia el objeto original lista2.clear() lista2.extend([999, 888, 777]) print(f" lista2 después de modificar: {lista2}") print("\n" + "="*60) print("=== REASIGNACIÓN VS MODIFICACIÓN ===") lista_a = [1, 2, 3] lista_b = [4, 5, 6] print(f"ANTES:") print(f" lista_a = {lista_a}") print(f" lista_b = {lista_b}") reasignar_vs_modificar(lista_a, lista_b) print(f"\nDESPUÉS:") print(f" lista_a = {lista_a} (sin cambios - fue reasignada)") print(f" lista_b = {lista_b} (¡CAMBIÓ! - fue modificada)") # ===== EL PELIGRO DE DEFAULT MUTABLE ARGUMENTS ===== print("\n" + "="*60) print("=== PELIGRO: DEFAULT MUTABLE ARGUMENTS ===") def funcion_peligrosa(elemento, lista=[]): """❌ MAL: Lista mutable como valor por defecto""" lista.append(elemento) return lista def funcion_segura(elemento, lista=None): """✅ BIEN: Usa None y crea lista nueva""" if lista is None: lista = [] lista.append(elemento) return lista print("❌ FUNCIÓN PELIGROSA con default mutable:") # Primera llamada resultado1 = funcion_peligrosa("primer") print(f" Primera llamada: {resultado1}") # Segunda llamada - ¡SORPRESA! resultado2 = funcion_peligrosa("segundo") print(f" Segunda llamada: {resultado2} (¡¡¡Contiene el anterior!!!)") # Tercera llamada - ¡Más sorpresas! resultado3 = funcion_peligrosa("tercero") print(f" Tercera llamada: {resultado3} (¡¡¡Acumula todo!!!)") print(f"\n Todas las variables apuntan al mismo objeto:") print(f" resultado1 is resultado2 is resultado3: {resultado1 is resultado2 is resultado3}") print("\n✅ FUNCIÓN SEGURA con None:") # Llamadas seguras resultado4 = funcion_segura("primer") print(f" Primera llamada: {resultado4}") resultado5 = funcion_segura("segundo") print(f" Segunda llamada: {resultado5} (solo contiene 'segundo')") resultado6 = funcion_segura("tercero") print(f" Tercera llamada: {resultado6} (solo contiene 'tercero')") # ===== DEMOSTRANDO IDs DE OBJETOS ===== print("\n" + "="*60) print("=== IDENTIDAD DE OBJETOS (id) ===") def mostrar_ids(inmutable, mutable): """Muestra los IDs de objetos para entender referencias""" print(f"🆔 IDs dentro de la función:") print(f" inmutable: {inmutable} (id: {id(inmutable)})") print(f" mutable: {mutable} (id: {id(mutable)})") # Modificar inmutable = inmutable + 100 mutable.append("modificado") print(f"🆔 IDs después de 'modificar':") print(f" inmutable: {inmutable} (id: {id(inmutable)}) ← NUEVO OBJETO") print(f" mutable: {mutable} (id: {id(mutable)}) ← MISMO OBJETO") numero_original = 42 lista_original = [1, 2, 3] print(f"🆔 IDs antes de llamar función:") print(f" numero_original: {numero_original} (id: {id(numero_original)})") print(f" lista_original: {lista_original} (id: {id(lista_original)})") mostrar_ids(numero_original, lista_original) print(f"\n🆔 IDs después de la función:") print(f" numero_original: {numero_original} (id: {id(numero_original)}) ← MISMO") print(f" lista_original: {lista_original} (id: {id(lista_original)}) ← MISMO, pero contenido cambió") # ===== TÉCNICAS PARA EVITAR MODIFICACIONES ACCIDENTALES ===== print("\n" + "="*60) print("=== TÉCNICAS PARA PROTEGER ARGUMENTOS ===") def funcion_protegida(lista_original): """Hace una copia para evitar modificar el original""" lista_copia = lista_original.copy() # También: list(lista_original) lista_copia.append("agregado_a_copia") print(f" Original: {lista_original}") print(f" Copia modificada: {lista_copia}") return lista_copia import copy def funcion_copia_profunda(estructura_compleja): """Para estructuras anidadas, usa deepcopy""" copia_profunda = copy.deepcopy(estructura_compleja) copia_profunda[0].append("nuevo") print(f" Original: {estructura_compleja}") print(f" Copia profunda: {copia_profunda}") return copia_profunda print("1. Protección con copy():") mi_lista_protegida = [1, 2, 3, 4] print(f" Antes: {mi_lista_protegida}") resultado_protegido = funcion_protegida(mi_lista_protegida) print(f" Después: {mi_lista_protegida} (sin cambios)") print("\n2. Protección con deepcopy() para estructuras anidadas:") estructura = [[1, 2], [3, 4]] print(f" Antes: {estructura}") resultado_profundo = funcion_copia_profunda(estructura) print(f" Después: {estructura} (sin cambios)") # ===== CASOS ESPECIALES: TUPLAS CON OBJETOS MUTABLES ===== print("\n" + "="*60) print("=== CASO ESPECIAL: TUPLAS CON MUTABLES ===") def modificar_tupla_con_lista(tupla_especial): """Las tuplas son inmutables, pero pueden contener objetos mutables""" print(f" Tupla recibida: {tupla_especial}") print(f" Tipo: {type(tupla_especial)}") # No podemos cambiar la tupla misma try: tupla_especial[0] = "nuevo" except TypeError as e: print(f" ❌ Error al modificar tupla: {e}") # Pero SÍ podemos modificar objetos mutables dentro de ella tupla_especial[1].append("nuevo_en_lista") tupla_especial[2]["nueva_clave"] = "nuevo_valor" print(f" Tupla después de modificar contenido: {tupla_especial}") tupla_mixta = ("inmutable", [1, 2, 3], {"clave": "valor"}) print(f"Tupla original: {tupla_mixta}") modificar_tupla_con_lista(tupla_mixta) print(f"Tupla después: {tupla_mixta} (contenido mutable cambió)") # ===== MEJORES PRÁCTICAS ===== print("\n" + "="*60) print("=== MEJORES PRÁCTICAS ===") print(""" ✅ RECOMENDACIONES: 1. 🚫 NUNCA uses objetos mutables como defaults: ❌ def func(lista=[]): # MAL ✅ def func(lista=None): # BIEN if lista is None: lista = [] 2. 📋 DOCUMENTA si tu función modifica argumentos: def modificar_lista(lista): '''Modifica la lista IN-PLACE agregando elemento''' lista.append("nuevo") 3. 🛡️ USA copias si quieres proteger originales: ✅ lista_trabajo = lista_original.copy() ✅ dict_trabajo = dict_original.copy() ✅ estructura_trabajo = copy.deepcopy(estructura_original) 4. 🔄 CONSIDERA devolver nuevos objetos en lugar de modificar: ✅ def agregar_elemento(lista, elemento): return lista + [elemento] # Nueva lista 5. 🏷️ USA type hints para claridad: from typing import List, Dict def procesar(datos: List[int]) -> List[int]: """) # ===== DEMOSTRACIÓN FINAL: TODAS LAS TÉCNICAS ===== print("\n" + "="*60) print("=== DEMOSTRACIÓN FINAL ===") # Función que demuestra todos los conceptos def funcion_completa( inmutable: int, mutable_modificable: list, mutable_protegido: list = None, **kwargs ) -> dict: """ Función que demuestra manejo correcto de argumentos Args: inmutable: Se puede 'modificar' sin afectar original mutable_modificable: Se modificará directamente mutable_protegido: Se copiará antes de modificar Returns: dict: Resumen de todas las operaciones """ # Manejar default mutable correctamente if mutable_protegido is None: mutable_protegido = [] # Crear copia para proteger original copia_protegida = mutable_protegido.copy() # Operaciones inmutable_nuevo = inmutable * 2 mutable_modificable.append("modificado_directamente") copia_protegida.append("modificado_en_copia") # Procesar kwargs si los hay configuraciones = {} for clave, valor in kwargs.items(): configuraciones[f"config_{clave}"] = valor return { "inmutable_original": inmutable, "inmutable_procesado": inmutable_nuevo, "mutable_modificado": mutable_modificable, "copia_protegida": copia_protegida, "configuraciones": configuraciones } # Datos de prueba numero = 10 lista_modificable = [1, 2, 3] lista_protegida = [10, 20, 30] print("Datos antes de la función:") print(f" numero: {numero}") print(f" lista_modificable: {lista_modificable}") print(f" lista_protegida: {lista_protegida}") # Llamar función resultado_final = funcion_completa( numero, lista_modificable, lista_protegida, debug=True, modo="produccion" ) print(f"\nDatos después de la función:") print(f" numero: {numero} (sin cambios)") print(f" lista_modificable: {lista_modificable} (MODIFICADA)") print(f" lista_protegida: {lista_protegida} (sin cambios)") print(f"\nResultado devuelto:") for clave, valor in resultado_final.items(): print(f" {clave}: {valor}") # ===== RESUMEN CONCEPTUAL FINAL ===== print("\n" + "="*60) print("=== RESUMEN CONCEPTUAL ===") print(""" 🔑 CONCEPTOS CLAVE: 📦 INMUTABLES (int, str, tuple, frozenset): • NO se pueden modificar después de crearse • "Modificar" crea un nuevo objeto • Seguros de usar como argumentos 🔓 MUTABLES (list, dict, set): • SÍ se pueden modificar después de crearse • Modificaciones afectan el objeto original • Requieren cuidado como argumentos 🆔 REFERENCIAS: • Los argumentos pasan referencias a objetos • Inmutables: nueva referencia al "modificar" • Mutables: misma referencia, objeto modificado ⚠️ DEFAULT MUTABLE ARGUMENTS: • Valores por defecto se evalúan UNA VEZ • Objetos mutables se reutilizan entre llamadas • Usar None + verificación dentro de función 🛡️ PROTECCIÓN: • .copy() para copias superficiales • copy.deepcopy() para copias profundas • Documentar cuando funciones modifican argumentos """)

Explicación: Los argumentos inmutables (int, str) crean nuevos objetos al "modificarse". Los mutables (list, dict) se modifican directamente. Los defaults mutables son peligrosos porque se reutilizan entre llamadas.

14
Básico de *args - Visualización paso a paso
Fácil
Problema: Crea funciones simples con *args para entender cómo Python empaqueta argumentos en una tupla. Copia el código en PythonTutor y observa cómo se forma la tupla args.

✅ Solución para PythonTutor:

# EJERCICIO 14: *args básico # Copia este código en pythontutor.com y ejecuta paso a paso def sumar_numeros(*args): print("Dentro de la función:") print(f"args = {args}") print(f"tipo de args = {type(args)}") total = 0 for numero in args: print(f"Sumando: {numero}") total = total + numero return total # Llamada 1: Con 3 números print("=== LLAMADA 1 ===") resultado1 = sumar_numeros(10, 20, 30) print(f"Resultado: {resultado1}") # Llamada 2: Con 1 número print("\n=== LLAMADA 2 ===") resultado2 = sumar_numeros(100) print(f"Resultado: {resultado2}") # Llamada 3: Sin números print("\n=== LLAMADA 3 ===") resultado3 = sumar_numeros() print(f"Resultado: {resultado3}") print("\n=== OBSERVA EN PYTHONTUTOR ===") print("1. Cómo args se convierte en tupla") print("2. Cómo cambia args en cada llamada") print("3. El bucle for iterando sobre args")

Para PythonTutor: Observa cómo los argumentos individuales (10, 20, 30) se empaquetan en la tupla args = (10, 20, 30). Ve paso a paso cómo el bucle itera sobre cada elemento.

15
Básico de **kwargs - Visualización paso a paso
Fácil
Problema: Crea funciones simples con **kwargs para entender cómo Python empaqueta argumentos con nombre en un diccionario. Observa en PythonTutor cómo se crea el diccionario kwargs.

✅ Solución para PythonTutor:

# EJERCICIO 15: **kwargs básico # Copia este código en pythontutor.com y ejecuta paso a paso def mostrar_informacion(**kwargs): print("Dentro de la función:") print(f"kwargs = {kwargs}") print(f"tipo de kwargs = {type(kwargs)}") print("Iterando sobre kwargs:") for clave, valor in kwargs.items(): print(f" {clave} = {valor}") # Llamada 1: Con información personal print("=== LLAMADA 1 ===") mostrar_informacion(nombre="Ana", edad=25, ciudad="Madrid") # Llamada 2: Con información diferente print("\n=== LLAMADA 2 ===") mostrar_informacion(producto="Laptop", precio=800, marca="Dell") # Llamada 3: Sin argumentos print("\n=== LLAMADA 3 ===") mostrar_informacion() # Ejemplo más simple para ver el empaquetado def simple_kwargs(**kwargs): print(f"Recibí: {kwargs}") return kwargs print("\n=== EJEMPLO SIMPLE ===") resultado = simple_kwargs(a=1, b=2, c=3) print(f"Devuelto: {resultado}") print("\n=== OBSERVA EN PYTHONTUTOR ===") print("1. Cómo los argumentos nombre=valor se convierten en diccionario") print("2. La estructura del diccionario kwargs") print("3. Cómo .items() devuelve pares clave-valor")

Para PythonTutor: Observa cómo los argumentos con nombre (nombre="Ana", edad=25) se empaquetan en el diccionario kwargs = {'nombre': 'Ana', 'edad': 25}. Ve cómo .items() itera sobre pares clave-valor.

16
Combinando *args y **kwargs - Visualización completa
Fácil
Problema: Crea una función que use tanto *args como **kwargs para ver cómo Python separa argumentos posicionales y con nombre. Ideal para visualizar el proceso completo en PythonTutor.

✅ Solución para PythonTutor:

# EJERCICIO 16: *args y **kwargs juntos # Copia este código en pythontutor.com y ejecuta paso a paso def funcion_completa(primer_arg, *args, **kwargs): print("=== DENTRO DE LA FUNCIÓN ===") print(f"primer_arg = {primer_arg}") print(f"args = {args}") print(f"kwargs = {kwargs}") print("\nTipos de datos:") print(f"tipo de args: {type(args)}") print(f"tipo de kwargs: {type(kwargs)}") print("\nProcesando args:") for i, valor in enumerate(args): print(f" args[{i}] = {valor}") print("\nProcesando kwargs:") for clave, valor in kwargs.items(): print(f" {clave} = {valor}") # Llamada 1: Argumentos mixtos print("=== LLAMADA 1 ===") funcion_completa("obligatorio", 10, 20, 30, nombre="Pedro", edad=28) # Llamada 2: Solo obligatorio y kwargs print("\n=== LLAMADA 2 ===") funcion_completa("solo_esto", color="rojo", activo=True) # Llamada 3: Solo obligatorio y args print("\n=== LLAMADA 3 ===") funcion_completa("primero", "segundo", "tercero", "cuarto") # Función simple para ver el empaquetado def ver_empaquetado(*args, **kwargs): return args, kwargs print("\n=== VER EMPAQUETADO ===") tupla_args, dict_kwargs = ver_empaquetado(1, 2, 3, a=10, b=20) print(f"args empaquetados: {tupla_args}") print(f"kwargs empaquetados: {dict_kwargs}") # Ejemplo con unpacking (desempaquetado) def recibir_tres(x, y, z): print(f"x={x}, y={y}, z={z}") print("\n=== UNPACKING ===") numeros = [100, 200, 300] print(f"Lista original: {numeros}") print("Llamando recibir_tres(*numeros):") recibir_tres(*numeros) datos = {"x": 1, "y": 2, "z": 3} print(f"\nDiccionario original: {datos}") print("Llamando recibir_tres(**datos):") recibir_tres(**datos) print("\n=== OBSERVA EN PYTHONTUTOR ===") print("1. Cómo se separan argumentos posicionales de los con nombre") print("2. La formación de tupla args y diccionario kwargs") print("3. El proceso de unpacking con * y **") print("4. Cómo los argumentos 'viajan' entre funciones")

Para PythonTutor: Este es el ejemplo más completo. Observa cómo Python separa automáticamente argumentos posicionales en args (tupla) y argumentos con nombre en kwargs (diccionario). Ve también el proceso inverso de unpacking.

🧪 Guía de Experimentación con Matplotlib

Proceso de aprendizaje recomendado:

1. Ejecuta el código base - Ve el gráfico inicial

2. Cambia UN parámetro - Modifica solo color, luego ejecuta

3. Observa el efecto - ¿Cómo cambió el gráfico?

4. Prueba otro parámetro - Ahora cambia el marker

5. Combina efectos - Color + marker + linestyle

6. Crea tu estilo - Inventa combinaciones únicas

⚡ Experimentos rápidos:

• Cambia color='blue'color='red'

• Cambia marker='o'marker='*'

• Cambia linestyle='-'linestyle='--'

• Cambia linewidth=2linewidth=5

🎯 Objetivo: Entender cómo **kwargs hace que matplotlib sea tan flexible y poderoso para visualización de datos.

17
Matplotlib básico con **kwargs - Experimenta con estilos
Fácil
Problema: Usa la función graficar(x, y, **kwargs) para crear gráficos personalizables con matplotlib. Experimenta cambiando diferentes parámetros para ver cómo afectan al gráfico visualmente.

✅ Solución:

import matplotlib.pyplot as plt def graficar(x, y, **kwargs): """Función que usa **kwargs para personalizar gráficos""" plt.figure(figsize=(10, 6)) plt.plot(x, y, **kwargs) plt.title("Ejemplo con **kwargs") plt.xlabel("Eje X") plt.ylabel("Eje Y") plt.grid(True) plt.legend() plt.show() # EXPERIMENTO 1: Gráfico básico print("🔵 EXPERIMENTO 1: Línea básica") graficar( [1, 2, 3, 4, 5], [2, 4, 6, 8, 10], color='blue', label='Línea básica' ) # EXPERIMENTO 2: Cambiar color y estilo print("🟣 EXPERIMENTO 2: Color y estilo diferente") graficar( [1, 2, 3, 4, 5], [2, 4, 6, 8, 10], color='purple', linestyle='--', linewidth=3, label='Línea discontinua' ) # EXPERIMENTO 3: Añadir marcadores print("🔴 EXPERIMENTO 3: Con marcadores") graficar( [1, 2, 3, 4, 5], [1, 4, 9, 16, 25], color='red', marker='o', markersize=8, linewidth=2, label='Con círculos' ) # ===== INSTRUCCIONES PARA EXPERIMENTAR ===== print(""" 🧪 EXPERIMENTA CAMBIANDO ESTOS PARÁMETROS: 📊 COLORES (color=): • 'red', 'blue', 'green', 'purple', 'orange', 'pink' • '#FF5733' (código hexadecimal) • (0.2, 0.6, 0.8) (RGB entre 0 y 1) 📏 ESTILOS DE LÍNEA (linestyle=): • '-' (línea sólida) • '--' (línea discontinua) • '-.' (línea punto-raya) • ':' (línea punteada) 🔲 MARCADORES (marker=): • 'o' (círculo) • 's' (cuadrado) • '^' (triángulo hacia arriba) • 'v' (triángulo hacia abajo) • '*' (estrella) • '+' (cruz) • 'D' (diamante) 📐 TAMAÑOS: • linewidth=1, 2, 3, 4, 5 • markersize=5, 8, 10, 12, 15 🏷️ ETIQUETAS: • label='Tu texto aquí' """) # Función para múltiples experimentos def experimentos_rapidos(): """Varios gráficos para comparar estilos""" # Datos comunes x = [1, 2, 3, 4, 5] y = [2, 3, 5, 7, 11] # Números primos # Crear subplots para comparar fig, axes = plt.subplots(2, 2, figsize=(12, 10)) fig.suptitle('Comparación de Estilos con **kwargs', fontsize=16) # Estilo 1: Línea simple axes[0,0].plot(x, y, color='blue', linewidth=2, label='Simple') axes[0,0].set_title('Estilo 1: Simple') axes[0,0].grid(True) axes[0,0].legend() # Estilo 2: Con marcadores axes[0,1].plot(x, y, color='red', marker='o', markersize=8, linewidth=2, label='Con marcadores') axes[0,1].set_title('Estilo 2: Con marcadores') axes[0,1].grid(True) axes[0,1].legend() # Estilo 3: Línea discontinua axes[1,0].plot(x, y, color='green', linestyle='--', linewidth=3, label='Discontinua') axes[1,0].set_title('Estilo 3: Discontinua') axes[1,0].grid(True) axes[1,0].legend() # Estilo 4: Combinado axes[1,1].plot(x, y, color='purple', linestyle='-.', marker='s', markersize=10, linewidth=2, label='Combinado') axes[1,1].set_title('Estilo 4: Combinado') axes[1,1].grid(True) axes[1,1].legend() plt.tight_layout() plt.show() # Ejecutar comparación print("\n📊 COMPARACIÓN DE ESTILOS:") experimentos_rapidos() # ===== DESAFÍOS PARA PROBAR ===== print(""" 🎯 DESAFÍOS PARA TI: 1. 🔵 Crea una línea AZUL con marcadores de ESTRELLA graficar([1,2,3,4], [10,20,15,25], color='blue', marker='*', label='Mi línea') 2. 🟠 Haz una línea NARANJA PUNTEADA muy GRUESA graficar([1,2,3,4], [5,10,15,20], color='orange', linestyle=':', linewidth=4, label='Gruesa') 3. 🟣 Combina: PÚRPURA + DISCONTINUA + TRIÁNGULOS + GROSOR 3 graficar([1,2,3,4], [2,8,6,12], color=?, linestyle=?, marker=?, linewidth=?, label=?) 4. 🌈 Usa un color HEXADECIMAL (#FF6B9D) con marcadores DIAMANTE graficar([1,2,3,4], [3,6,9,12], color='#FF6B9D', marker='D', label='Hexadecimal') 5. 🎨 Inventa tu propia combinación única ¡Copia el código, cámbialo y observa cómo cambia el gráfico! """) # Datos de ejemplo para usar datos_ejemplos = { 'lineales': ([1, 2, 3, 4, 5], [2, 4, 6, 8, 10]), 'cuadraticos': ([1, 2, 3, 4, 5], [1, 4, 9, 16, 25]), 'aleatorios': ([1, 2, 3, 4, 5], [3, 7, 2, 9, 5]), 'seno': ([0, 1, 2, 3, 4], [0, 0.84, 0.91, 0.14, -0.76]) } print(f"\n📋 DATOS DE EJEMPLO PARA USAR:") for nombre, (x, y) in datos_ejemplos.items(): print(f" {nombre}: x={x}, y={y}")

Explicación: **kwargs permite pasar cualquier parámetro de estilo a matplotlib. Experimenta cambiando color, linestyle, marker, linewidth y markersize para ver cómo afectan visualmente al gráfico.

18
Gráficos avanzados con **kwargs - Múltiples series
Medio
Problema: Crea una función que permita graficar múltiples series de datos usando **kwargs para personalizar cada línea. Aprende a combinar diferentes estilos en un solo gráfico.

✅ Solución:

import matplotlib.pyplot as plt import math def graficar_multiple(*series, titulo="Gráfico Multiple", **config_general): """ Grafica múltiples series de datos series: tuplas de (x, y, **kwargs_linea) config_general: configuración del gráfico completo """ # Configurar el gráfico figsize = config_general.get('figsize', (12, 8)) grid = config_general.get('grid', True) plt.figure(figsize=figsize) # Graficar cada serie for i, serie in enumerate(series): if len(serie) >= 2: x, y = serie[0], serie[1] # Si hay kwargs específicos para esta línea kwargs_linea = serie[2] if len(serie) > 2 else {} plt.plot(x, y, **kwargs_linea) plt.title(titulo, fontsize=16) plt.xlabel("Eje X", fontsize=12) plt.ylabel("Eje Y", fontsize=12) if grid: plt.grid(True, alpha=0.3) plt.legend() plt.tight_layout() plt.show() # ===== EXPERIMENTO 1: Comparar funciones matemáticas ===== print("📊 EXPERIMENTO 1: Funciones matemáticas") # Datos x = list(range(0, 11)) # [0, 1, 2, ..., 10] y_lineal = [2*i for i in x] # y = 2x y_cuadratica = [i**2 for i in x] # y = x² y_raiz = [math.sqrt(i) for i in x] # y = √x graficar_multiple( (x, y_lineal, {'color': 'blue', 'linewidth': 2, 'label': 'y = 2x'}), (x, y_cuadratica, {'color': 'red', 'linestyle': '--', 'linewidth': 2, 'label': 'y = x²'}), (x, y_raiz, {'color': 'green', 'marker': 'o', 'linewidth': 2, 'label': 'y = √x'}), titulo="Comparación de Funciones" ) # ===== EXPERIMENTO 2: Datos de ventas por trimestre ===== print("\n💰 EXPERIMENTO 2: Ventas por trimestre") meses = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun'] ventas_2023 = [100, 120, 110, 130, 125, 140] ventas_2024 = [105, 125, 135, 145, 150, 160] ventas_2025 = [110, 130, 140, 155, 165, 175] # Función específica para gráficos de ventas def graficar_ventas(meses, *datos_anuales, **estilos): """Gráfico especializado para ventas""" plt.figure(figsize=(12, 7)) # Estilos predefinidos para cada año estilos_default = [ {'color': 'blue', 'marker': 'o', 'linewidth': 2}, {'color': 'red', 'marker': 's', 'linewidth': 2}, {'color': 'green', 'marker': '^', 'linewidth': 2} ] for i, (año, ventas) in enumerate(datos_anuales): estilo = estilos_default[i % len(estilos_default)] # Permitir override de estilos estilo.update(estilos.get(f'año_{i+1}', {})) estilo['label'] = año plt.plot(meses, ventas, **estilo) plt.title('Ventas por Mes', fontsize=16) plt.xlabel('Mes', fontsize=12) plt.ylabel('Ventas (miles €)', fontsize=12) plt.grid(True, alpha=0.3) plt.legend() plt.xticks(rotation=45) plt.tight_layout() plt.show() # Llamar función de ventas graficar_ventas( meses, ('2023', ventas_2023), ('2024', ventas_2024), ('2025', ventas_2025), año_1={'linestyle': '--'}, # Personalizar 2023 año_3={'marker': '*', 'markersize': 10} # Personalizar 2025 ) # ===== EXPERIMENTO 3: Datos experimentales con barras de error ===== print("\n🔬 EXPERIMENTO 3: Datos experimentales") def graficar_experimental(x, y, errores=None, **kwargs): """Gráfico con barras de error usando **kwargs""" plt.figure(figsize=(10, 6)) # Parámetros por defecto defaults = { 'color': 'blue', 'marker': 'o', 'linewidth': 2, 'markersize': 8, 'capsize': 5 } # Combinar defaults con kwargs del usuario estilo = {**defaults, **kwargs} # Separar parámetros de errorbar de los de plot errorbar_params = ['capsize', 'capthick', 'ecolor', 'elinewidth'] plot_params = {k: v for k, v in estilo.items() if k not in errorbar_params} error_params = {k: v for k, v in estilo.items() if k in errorbar_params} if errores: plt.errorbar(x, y, yerr=errores, **plot_params, **error_params) else: plt.plot(x, y, **plot_params) plt.title('Datos Experimentales') plt.xlabel('Tiempo (s)') plt.ylabel('Temperatura (°C)') plt.grid(True, alpha=0.3) plt.legend() plt.show() # Datos experimentales tiempo = [0, 1, 2, 3, 4, 5] temperatura = [20, 25, 30, 28, 32, 35] errores = [0.5, 0.8, 1.0, 0.7, 1.2, 0.9] graficar_experimental( tiempo, temperatura, errores, color='red', marker='s', label='Medición A', ecolor='black', capsize=8 ) # ===== DESAFÍOS PRÁCTICOS ===== print(""" 🎯 DESAFÍOS PRÁCTICOS: 1. 📈 GRÁFICO DE RENDIMIENTO: Crea un gráfico que compare el rendimiento de 3 algoritmos: - Algoritmo A: [1, 2, 4, 8, 16] (azul, círculos) - Algoritmo B: [1, 3, 6, 12, 20] (rojo, cuadrados) - Algoritmo C: [1, 2, 3, 5, 8] (verde, triángulos) 2. 🌡️ TEMPERATURAS MENSUALES: Grafica temperaturas de 3 ciudades con diferentes estilos: - Madrid: línea sólida azul - Barcelona: línea discontinua roja - Sevilla: línea punteada verde 3. 📊 TU PROPIO DASHBOARD: Combina múltiples tipos de datos en un gráfico personalizado INSTRUCCIONES PASO A PASO: """) # Plantilla para experimentar print(""" 🧪 PLANTILLA PARA EXPERIMENTAR: # Cambia estos valores y observa los resultados: graficar_multiple( ([1,2,3,4], [10,20,15,25], { 'color': '???', # Prueba: 'blue', 'red', '#FF5733' 'linestyle': '???', # Prueba: '-', '--', '-.', ':' 'marker': '???', # Prueba: 'o', 's', '^', '*' 'linewidth': ???, # Prueba: 1, 2, 3, 4, 5 'markersize': ???, # Prueba: 5, 8, 10, 12 'label': '???' # Tu etiqueta personalizada }), titulo="Tu título aquí" ) 🔄 PASOS PARA EXPERIMENTAR: 1. Copia el código base 2. Cambia UN parámetro a la vez 3. Ejecuta y observa el cambio 4. Anota qué efecto tiene cada parámetro 5. Combina múltiples cambios 6. ¡Crea tu estilo único! 💡 IDEAS PARA PROBAR: • Color + marcador + línea discontinua • Línea muy gruesa sin marcadores • Solo marcadores (linewidth=0) • Colores hexadecimales (#FF6B9D) • Combinaciones de varios estilos """) # Función de ayuda rápida def estilos_disponibles(): """Muestra todos los estilos disponibles""" print("🎨 REFERENCIA RÁPIDA DE ESTILOS:") print("━" * 50) colores = ['red', 'blue', 'green', 'purple', 'orange', 'pink', 'brown', 'gray'] lineas = ['-', '--', '-.', ':'] marcadores = ['o', 's', '^', 'v', '*', '+', 'D', 'x'] print(f"🎨 Colores: {', '.join(colores)}") print(f"📏 Líneas: {', '.join(lineas)} (sólida, discontinua, punto-raya, punteada)") print(f"🔲 Marcadores: {', '.join(marcadores)}") print(f"📐 Grosores: 1, 2, 3, 4, 5 (linewidth)") print(f"📏 Tamaños: 5, 8, 10, 12, 15 (markersize)") estilos_disponibles()

Explicación: **kwargs permite crear funciones flexibles que aceptan cualquier parámetro de matplotlib. Esto es especialmente útil para crear dashboards y gráficos personalizables donde el usuario puede experimentar con diferentes estilos visuales.