Las tres funciones que escribimos en la lección anterior —mostrar_cabecera(), mostrar_menu() y pausar()— tienen una limitación evidente: hacen siempre exactamente lo mismo. mostrar_menu() pinta ese menú y no otro; pausar() muestra ese mensaje y no otro. Son útiles, pero rígidas. Y por eso mismo no nos han servido para el problema de fondo de TareaFácil: los cuatro bucles de validación repetidos, que se parecen entre sí pero no son idénticos —cada uno tiene su mensaje, sus valores aceptables y su tipo de dato.

Para extraer eso hacen falta las dos piezas que faltan. La primera, decirle cosas a la función cuando la llamas: eso son los parámetros, y convierten una función que hace una cosa en una función que hace una familia entera de cosas. La segunda, que la función te entregue un resultado: eso es return, y es lo que permite usar el valor calculado en cualquier sitio, guardarlo en una variable o pasarlo a otra función.

Con parámetros y return, una función deja de ser un atajo para escribir menos y se convierte en una pieza de construcción: algo con entradas y salidas bien definidas que puedes montar con otras piezas. Esta es la lección más importante del módulo.

Contenido

  1. Parámetros y argumentos: dos palabras para dos cosas
  2. Argumentos posicionales y argumentos por nombre
  3. Valores por defecto
  4. return: devolver un valor y terminar
  5. Varios return y el return temprano
  6. Devolver más de un valor
  7. ¿Imprimir dentro o devolver?
  8. *args y **kwargs, para reconocerlos
  9. TareaFácil: las funciones de validación
  10. Errores comunes y consejos
  11. Ejercicios
  12. Conclusión

  1. Parámetros y argumentos: dos palabras para dos cosas

Un parámetro es el nombre que aparece entre los paréntesis de la definición; un argumento es el valor concreto que se entrega entre los paréntesis de la llamada.

def saludar(nombre):            # 'nombre' es el PARAMETRO
    print(f"Hola, {nombre}. Bienvenida a Estudio Alba.")

saludar("Marta")                # "Marta" es el ARGUMENTO
saludar("Luis")
Hola, Marta. Bienvenida a Estudio Alba.
Hola, Luis. Bienvenida a Estudio Alba.

Lo que ocurre en la llamada es exactamente esto: Python crea, dentro de la función, una variable llamada nombre y le asigna el valor "Marta" —una asignación normal y corriente, nombre = "Marta", hecha automáticamente por ti. Esa variable vive solo mientras la función se ejecuta y desaparece al terminar; el porqué exacto es el tema de Ámbito de variables.

Parámetro Argumento
Dónde aparece En la definición (def) En la llamada
Qué es Un nombre, un hueco por rellenar Un valor concreto
Cuántos hay Fijos al escribir la función Uno por cada parámetro, en cada llamada
Ejemplo def saludar(nombre): saludar("Nuria")

Distinguirlos importa porque los mensajes de error de Python hablan en estos términos: llamar a saludar() sin nada produce TypeError: saludar() missing 1 required positional argument: 'nombre', es decir, «falta 1 argumento obligatorio y se llama nombre». Si pasas de más, dirá takes 1 positional argument but 2 were given. En cuanto entiendes el vocabulario, estos errores dejan de dar miedo y pasan a ser instrucciones bastante precisas.

Una función puede tener varios parámetros, separados por comas, y entonces el orden manda:

def presentar_tarea(titulo, responsable, dias):
    print(f"{titulo} -> {responsable} ({dias} dias)")

presentar_tarea("Cartel feria del libro", "Nuria", 4)
presentar_tarea("Nuria", 4, "Cartel feria del libro")   # valido, pero absurdo
Cartel feria del libro -> Nuria (4 dias)
Nuria -> 4 (Cartel feria del libro dias)

La segunda llamada no da error: Python asigna por posición sin juzgar si tiene sentido. Es un fallo silencioso, de los peores, y la sección siguiente ofrece la defensa.

  1. Argumentos posicionales y argumentos por nombre

Hasta ahora hemos pasado argumentos posicionales: el primer valor va al primer parámetro, el segundo al segundo, y así. Pero Python permite también nombrar el parámetro al que va cada valor. Se llaman argumentos por nombre o keyword arguments:

presentar_tarea(titulo="Menu de la Panaderia Sole", responsable="Luis", dias=3)
presentar_tarea(responsable="Luis", dias=3, titulo="Menu de la Panaderia Sole")

Las dos llamadas hacen exactamente lo mismo: al indicar el nombre, el orden deja de importar. Y de paso, la llamada se lee sola. Compara crear_aviso("Cartel feria", True, False, 3) con crear_aviso("Cartel feria", urgente=True, silencioso=False, dias=3): sin los nombres, para entender la primera tendrías que ir a buscar la definición de crear_aviso; con ellos, la llamada se explica en el sitio donde está.

Se pueden mezclar los dos estilos, con una regla firme: los posicionales van siempre antes que los nombrados.

presentar_tarea("Cartel feria", responsable="Nuria", dias=4)   # correcto
presentar_tarea(titulo="Cartel feria", "Nuria", 4)             # SyntaxError

Criterio práctico: usa argumentos por nombre cuando el valor sea un True/False suelto, un número sin unidades evidentes o cuando la función tenga más de dos o tres parámetros; un True desnudo en una llamada es una adivinanza para quien lea el código dentro de un mes.

  1. Valores por defecto

Un parámetro puede llevar un valor por defecto, que se usa si quien llama no proporciona ese argumento. Se escribe con un = en la definición:

def avisar(mensaje, veces=1):
    """Repite un aviso por pantalla el numero de veces indicado."""
    for i in range(veces):
        print(f"[AVISO] {mensaje}")

avisar("Revisar el encargo del cliente Vidal")   # imprime 1 linea
avisar("Entrega manana", veces=3)               # imprime 3 lineas

veces se ha vuelto opcional: la primera llamada usa el valor por defecto, 1, y la segunda lo sobrescribe. Así se consigue que una función sea sencilla en el caso normal y flexible en el especial, que es exactamente lo que quieres. Hay una regla sintáctica que Python impone: los parámetros con valor por defecto van al final, después de todos los obligatorios. Es decir, def pedir_texto(mensaje, obligatorio=True): es correcto, pero def pedir_texto(obligatorio=True, mensaje): produce SyntaxError: parameter without a default follows parameter with a default. La razón es de sentido común: si los opcionales pudieran ir en medio, Python no sabría a qué parámetro corresponde el segundo valor de una llamada posicional.

Un aviso que conviene grabar desde hoy, aunque su explicación completa llegue más adelante: no uses como valor por defecto un objeto mutable (una lista, por ejemplo). El valor por defecto se evalúa una sola vez, al definir la función, no en cada llamada; si es mutable y se modifica dentro, esa modificación persiste entre llamadas y produce un comportamiento desconcertante. Con valores inmutables —números, cadenas, booleanos, None— no hay ningún problema, y es lo único que usaremos por ahora. Volveremos a ello cuando conozcas las listas.

  1. return: devolver un valor y terminar

return hace dos cosas a la vez, y ambas importan:

  1. Entrega un valor a quien llamó a la función.
  2. Termina la función inmediatamente: ninguna línea posterior del cuerpo se ejecuta.
def calcular_coste(dias, tarifa_diaria):
    """Devuelve el coste total de una tarea, sin imprimir nada."""
    return dias * tarifa_diaria

coste = calcular_coste(4, 120.0)
print(f"Coste estimado: {coste:.2f} EUR")
print(f"Con IVA: {coste * 1.21:.2f} EUR")
Coste estimado: 480.00 EUR
Con IVA: 580.80 EUR

Fíjate en lo que ha ganado el programa. calcular_coste no sabe ni le importa qué se hará con el número: lo entrega y se aparta. Quien llama lo guarda en coste, lo formatea y lo reutiliza para el IVA. Una llamada a una función que devuelve valor es una expresión, con todo lo que eso significa: puede ir dentro de un print, de un if, de una operación aritmética o dentro de otra llamada.

print(calcular_coste(2, 90.0))                               # dentro de print
if calcular_coste(10, 120.0) > 1000:                         # dentro de una condicion
    print("Presupuesto alto: requiere aprobacion de Marta.")
total = calcular_coste(3, 100.0) + calcular_coste(2, 80.0)   # en una suma

Y la segunda mitad de return, la de terminar, se ve así: si escribes return 42 y debajo un print("..."), ese print es código muerto y no se ejecuta jamás; los editores suelen marcarlo en gris. Si te aparece uno, o sobra o el return está en el sitio equivocado.

  1. Varios return y el return temprano

Una función puede tener tantos return como haga falta. Se ejecuta el primero que se alcance, y ahí termina todo:

def etiqueta_de_carga(dias):
    """Devuelve una etiqueta de carga de trabajo segun los dias estimados."""
    if dias <= 2:
        return "ligera"
    if dias <= 5:
        return "media"
    return "pesada"

print(etiqueta_de_carga(1))    # ligera
print(etiqueta_de_carga(4))    # media
print(etiqueta_de_carga(9))    # pesada

Fíjate en que no hay ni elif ni else, y sin embargo funciona: como cada rama termina en return, si la primera condición se cumple la función ya ha terminado y nunca llega a la segunda. Esta es la cláusula de guarda de 03-01 llevada a las funciones: comprobar primero los casos particulares, salir en cuanto se resuelven y dejar el caso general al final, sin indentar. Se lee mucho mejor que un if anidado de tres niveles.

Un return sin valor —solo la palabra— también existe: termina la función y devuelve None. Es útil para abandonar pronto un procedimiento:

def mostrar_ficha(hay_tarea, titulo):
    if not hay_tarea:
        print("Todavia no hay ninguna tarea registrada.")
        return                       # salimos: no hay nada que pintar
    print(f"Ficha de: {titulo}")

Cuidado con un despiste habitual: si una rama olvida su return, esa rama devuelve None en silencio, y la función parece correcta hasta que alguien la llama con el valor que cae justo ahí. Comprueba una por una que todas las ramas devuelven algo.

  1. Devolver más de un valor

Si separas varios valores con comas después de return, la función los devuelve todos a la vez:

def analizar_titulo(texto):
    """Devuelve el titulo normalizado y su numero de caracteres."""
    limpio = texto.strip().capitalize()
    return limpio, len(limpio)

titulo, longitud = analizar_titulo("   cartel feria del libro  ")
print(titulo)      # Cartel feria del libro
print(longitud)    # 22

Lo que ocurre por dentro es que Python empaqueta los dos valores en un único objeto llamado tupla, y la línea titulo, longitud = ... lo desempaqueta en dos variables. Puedes comprobarlo guardando el resultado en una sola variable: resultado = analizar_titulo("cartel feria") deja en resultado el valor ('Cartel feria', 12), cuyo type() es <class 'tuple'>. Las tuplas son el tema de Tuplas y estructuras anidadas; hoy basta con saber que el desempaquetado funciona y que el número de variables a la izquierda debe coincidir con el de valores devueltos, o tendrás un ValueError: too many values to unpack.

Un consejo de diseño: devolver dos valores está bien; devolver cinco es señal de que esa función hace demasiado, o de que esos cinco valores son en realidad un solo concepto que todavía no tiene nombre —justo lo que le pasa a TareaFácil con titulo, responsable, prioridad, dias y completada, y que se resolverá en los módulos 5 y 7.

  1. ¿Imprimir dentro o devolver?

Esta es la decisión de diseño más frecuente al escribir una función, y la respuesta correcta casi siempre es la misma. Compara una coste_impreso(dias, tarifa) cuyo cuerpo es print(f"Coste: {dias * tarifa:.2f} EUR") con una coste_devuelto(dias, tarifa) cuyo cuerpo es return dias * tarifa:

Criterio Imprimir dentro Devolver el valor
¿Se puede reutilizar el resultado? No: se ha ido a la pantalla Sí: es un valor como cualquier otro
¿Sirve para calcular otra cosa? No Sí (total = a() + b())
¿Se puede probar sin mirar la pantalla? No Sí: se compara el valor devuelto
¿Sirve para un informe, un fichero, otra pantalla? No: el formato está fijado Sí: decide quien llama
¿Quién decide el formato? La función, para siempre Quien la usa, en cada caso

Devolver es casi siempre mejor, y la razón de fondo es que una función que imprime ha tomado por ti una decisión que no le correspondía. coste_impreso solo sirve para mostrar euros con dos decimales en la pantalla; el día que quieras el coste para sumarlo, compararlo o guardarlo, no te vale. coste_devuelto sirve para todo eso y también para imprimir, porque siempre puedes hacer print(f"{coste_devuelto(4, 120):.2f} EUR"). La consecuencia práctica es una regla que retomaremos en Descomponer un programa en funciones: las funciones que calculan no imprimen; las que presentan no calculan. Y la que sí debe imprimir es precisamente la que dialoga con el usuario, como pedir_texto, que necesita mostrar el mensaje para poder preguntar.

  1. *args y **kwargs, para reconocerlos

Verás estas dos formas por todas partes en el código ajeno, así que conviene reconocerlas aunque no las uses todavía.

Un parámetro precedido de un asterisco recoge todos los argumentos posicionales sobrantes, y uno precedido de dos asteriscos recoge todos los argumentos por nombre sobrantes:

def anunciar(*mensajes):
    """Imprime por pantalla cuantos mensajes se le pasen."""
    for mensaje in mensajes:
        print(f"[AVISO] {mensaje}")

anunciar("Reunion a las 10", "Entregar el cartel", "Llamar a Vidal")
[AVISO] Reunion a las 10
[AVISO] Entregar el cartel
[AVISO] Llamar a Vidal

La versión con dos asteriscos se define igual —def registrar_evento(accion, **detalles):— y se llama con registrar_evento("alta_tarea", responsable="Nuria", dias=4); dentro, detalles asocia cada nombre con su valor.

Los nombres args y kwargs son pura convención (de arguments y keyword arguments); lo que hace el trabajo son los asteriscos. Por dentro, *args es una tupla y **kwargs un diccionario, dos estructuras del módulo 5: no necesitas dominarlas hoy, solo reconocer que def funcion(*args, **kwargs): acepta cualquier cosa. Úsalo con moderación: una función con parámetros explícitos se documenta y se entiende mucho mejor.

  1. TareaFácil: las funciones de validación

Llegó el momento de matar la repetición. Los cuatro bucles de validación de la v0.6 se reducen a tres funciones parametrizadas, más una que clasifica.

def pedir_texto(mensaje, obligatorio=True):
    """Pide un texto por teclado y lo devuelve ya limpio de espacios."""
    valor = input(mensaje).strip()
    while obligatorio and valor == "":
        valor = input("No puede estar vacio. " + mensaje).strip()
    return valor


def pedir_opcion(mensaje, opciones):
    """Pide un valor hasta que este dentro de las opciones admitidas."""
    valor = input(mensaje).strip().lower()
    while valor not in opciones:
        valor = input(f"No valido. Admitidos: {', '.join(opciones)}. ").strip().lower()
    return valor


def pedir_entero(mensaje, minimo, maximo):
    """Pide un entero y no lo devuelve hasta que este entre minimo y maximo."""
    texto = input(mensaje).strip()
    while not texto.isdigit() or not minimo <= int(texto) <= maximo:
        texto = input(f"Debe ser un entero entre {minimo} y {maximo}. ").strip()
    return int(texto)


def clasificar_urgencia(prioridad, dias):
    """Devuelve la etiqueta de urgencia que corresponde a la tarea."""
    if prioridad == "alta" and dias <= 2:
        return "CRITICA"
    if prioridad == "alta":
        return "Urgente"
    if prioridad == "media" and dias <= 3:
        return "Atencion"
    return "Normal"

Y así se usan desde el programa principal:

PRIORIDADES = ("alta", "media", "baja")
EQUIPO = ("marta", "luis", "nuria")

titulo = pedir_texto("Titulo        : ")
responsable = pedir_opcion("Responsable   : ", EQUIPO).capitalize()
prioridad = pedir_opcion("Prioridad     : ", PRIORIDADES)
dias = pedir_entero("Dias (1-365)  : ", 1, 365)
print(f"{titulo} [{clasificar_urgencia(prioridad, dias)}]")

Repasa lo que ha pasado, porque es mucho:

  • Cuatro bucles de validación repetidos han quedado en tres funciones, cada una escrita una sola vez. Si mañana quieres cambiar el mensaje de reproche, lo cambias en un sitio y cambia en todos.
  • El programa principal se lee como una lista de intenciones. La mecánica del while ha desaparecido de la vista sin desaparecer del programa.
  • pedir_opcion sirve para el responsable, para la prioridad y para la opción del menú, tres cosas que en la v0.6 eran tres bloques distintos. Ese es el poder de un parámetro: opciones convierte una función en una familia de funciones. Y obligatorio=True deja abierta la puerta a pedir un dato opcional el día que haga falta, sin tocar ninguna llamada actual.
  • clasificar_urgencia devuelve, no imprime. Por eso podemos meterla dentro de una f-string, y mañana podríamos usarla también para decidir un color o para ordenar tareas.

Fíjate además en un detalle de reparto de responsabilidades: pedir_opcion normaliza a minúsculas, pero el .capitalize() del responsable se aplica fuera, en quien llama. La función se ocupa de validar; el formato de presentación es asunto del programa. Y observa que pedir_entero devuelve ya un int, no un texto: convierte una vez, en el único sitio donde consta que el dato es convertible, aplicando el marco «presencia, tipo, dominio» de 02-04 de principio a fin.

Errores Comunes y Consejos

Olvidar el return. La función calcula bien y devuelve None. Síntoma: TypeError: unsupported operand type(s) for +: 'NoneType' and 'int' un poco más adelante, en la línea que intenta usar el resultado.

Confundir print con return. Escribir print(dias * tarifa) dentro de la función y luego coste = calcular_coste(...) deja coste a None, aunque el número correcto haya salido por pantalla. Ver el valor en la consola no es lo mismo que tenerlo.

Número de argumentos incorrecto. TypeError: ... missing 1 required positional argument o takes 2 positional arguments but 3 were given. El mensaje dice cuántos esperaba y cuántos recibió: cuéntalos en la definición.

Poner un parámetro con defecto antes de uno obligatorio. SyntaxError inmediato: los opcionales van al final, siempre. Y no uses una lista como valor por defecto: def f(x, historial=[]) comparte la misma lista entre todas las llamadas. Aún no manejas listas; recuérdalo para cuando llegue 05-01.

Poner código después del return. No se ejecuta jamás. Si lo has escrito, es que querías otra cosa.

Consejo: escribe primero la llamada. Antes de implementar, escribe cómo te gustaría usarla: dias = pedir_entero("Dias: ", 1, 365). Si esa línea se lee bien, la firma es buena; si te obliga a explicarla, cámbiala antes de escribir el cuerpo.

Consejo: dos o tres parámetros es lo cómodo. A partir de cuatro, la llamada se vuelve un jeroglífico y conviene usar argumentos por nombre —o preguntarse si esa función no estará haciendo dos cosas.

Ejercicios

Ejercicio 1: Predecir y explicar

Di qué imprime cada bloque y por qué. Hay un fallo de diseño escondido.

def media_dias(a, b):
    print((a + b) / 2)

def media_dias_2(a, b):
    return (a + b) / 2

x = media_dias(4, 6)
y = media_dias_2(4, 6)
print(x)
print(y)
print(y * 2)

Ejercicio 2: Presupuesto del estudio

Escribe tres funciones con docstring de una línea:

  • coste_tarea(dias, tarifa=110.0): devuelve el coste de una tarea; la tarifa por defecto es 110 euros al día.
  • aplicar_descuento(coste, porcentaje=0): devuelve el coste con el descuento aplicado. Si el porcentaje no está entre 0 y 50, devuelve el coste sin tocar.
  • resumen_presupuesto(titulo, dias, tarifa=110.0, descuento=0): usa las dos anteriores y devuelve dos valores: el texto de resumen y el importe final.

Después, calcula el presupuesto de «Cartel feria del libro», 4 días, tarifa por defecto y 10 % de descuento, e imprime el resumen desde el programa principal (no desde dentro de las funciones).

Ejercicio 3: Reparar la función

Esta función tiene tres defectos. Encuéntralos, explícalos y reescríbela.

def clasificar(dias, prioridad="alta", umbral):
    if prioridad == "alta":
        print("Urgente")
    elif dias > 10:
        return "Largo plazo"
    return "Normal"
    print("Clasificacion terminada")

Soluciones

Solución 1.

5.0
None
5.0
10.0

media_dias(4, 6) imprime 5.0 desde dentro y devuelve None, así que x vale None. media_dias_2(4, 6) no imprime nada pero devuelve 5.0, que se guarda en y. Luego print(x) muestra None, print(y) muestra 5.0 y print(y * 2) muestra 10.0. El fallo de diseño es media_dias: al imprimir en lugar de devolver, su resultado no se puede reutilizar. Comprueba que x * 2 daría TypeError, mientras que y * 2 funciona sin problema.

Solución 2.

def coste_tarea(dias, tarifa=110.0):
    """Devuelve el coste bruto de una tarea de la duracion indicada."""
    return dias * tarifa


def aplicar_descuento(coste, porcentaje=0):
    """Devuelve el coste con descuento; ignora porcentajes fuera de 0-50."""
    if not 0 <= porcentaje <= 50:
        return coste
    return coste * (1 - porcentaje / 100)


def resumen_presupuesto(titulo, dias, tarifa=110.0, descuento=0):
    """Devuelve el texto de resumen y el importe final del presupuesto."""
    bruto = coste_tarea(dias, tarifa)
    final = aplicar_descuento(bruto, descuento)
    texto = f"{titulo}: {dias} dias x {tarifa:.2f} = {bruto:.2f} EUR"
    return texto, final


detalle, importe = resumen_presupuesto("Cartel feria del libro", 4, descuento=10)
print(detalle)                                        # ... 4 dias x 110.00 = 440.00 EUR
print(f"Total con descuento: {importe:.2f} EUR")      # Total con descuento: 396.00 EUR

Tres detalles: descuento=10 se pasa por nombre para saltarse tarifa sin repetir su valor por defecto; ninguna de las tres funciones imprime, así que las tres son reutilizables; y la comparación encadenada 0 <= porcentaje <= 50 es la de 03-01, dentro de una guarda con return temprano.

Solución 3.

Defecto Explicación
umbral sin defecto va detrás de prioridad="alta" SyntaxError al cargar el fichero
La primera rama imprime en vez de devolver La función devuelve None cuando la prioridad es alta
El print final está detrás de un return Código muerto: no se ejecuta nunca
def clasificar(dias, umbral, prioridad="alta"):
    """Devuelve la clasificacion de una tarea segun prioridad y duracion."""
    if prioridad == "alta":
        return "Urgente"
    if dias > umbral:
        return "Largo plazo"
    return "Normal"

El parámetro obligatorio pasa delante, la primera rama devuelve como las demás, el código muerto desaparece y de paso umbral se usa de verdad, que en el original ni siquiera aparecía en el cuerpo.

Conclusión

Tus funciones ya dialogan. Los parámetros son los huecos que declaras en el def; los argumentos son los valores que entregas en la llamada, y pueden ir por posición o por nombre, siendo esta segunda forma la que hace legibles las llamadas con booleanos o con muchos datos. Un parámetro puede llevar valor por defecto, y entonces se vuelve opcional, con la regla de que los opcionales van al final y nunca son mutables. Del otro lado, return entrega un valor y termina la función en el acto; puede haber varios en distintas ramas, sirve como cláusula de guarda para salir pronto, y admite varios valores separados por comas que se desempaquetan al recibirlos. Y la regla que ordena todo lo demás: una función que calcula devuelve, no imprime, porque el valor sirve para todo y la pantalla solo para mirar.

TareaFácil ha dado el salto más grande de su historia. Los cuatro bucles de validación duplicados se han convertido en pedir_texto, pedir_opcion y pedir_entero, tres funciones que se escriben una vez y se usan desde cualquier sitio, y clasificar_urgencia calcula una etiqueta sin ensuciar la pantalla. El programa principal ha pasado de ser una maraña de while a una lista de intenciones legible. Pero al reunir todas esas funciones en un fichero aparecen preguntas nuevas y muy concretas. ¿Por qué pedir_opcion puede leer la constante PRIORIDADES sin que se la pasemos? ¿Qué pasaría si dentro de una función asignase titulo = "otro": cambiaría el titulo del programa principal? ¿Y por qué la variable valor de pedir_texto no existe fuera? Todo eso es una sola cuestión —dónde vive cada nombre— y tiene un nombre propio: el ámbito. Es la próxima lección: Ámbito de variables.

Fundamentos de la Programación

Módulo 1: Introducción a la Programación

Módulo 2: Conceptos Básicos

Módulo 3: Estructuras de Control

Módulo 4: Funciones y Procedimientos

Módulo 5: Estructuras de Datos

Módulo 6: Algoritmos Básicos

Módulo 7: Objetos y Organización del Código

Módulo 8: Buenas Prácticas y Herramientas

Módulo 9: Proyecto Final y Cierre del Curso

© Copyright 2026. Todos los derechos reservados