Al cerrar el módulo 2 dejamos un diagnóstico claro: el código de Papyrus funciona, pero vive suelto. La búsqueda de libros está dentro de menu.py, el cálculo del precio de socio se repite en venta_socio.py y en ticket.py, y cada mejora obliga a Ana a tocar varios scripts. La solución prometida son las funciones: bloques de código con nombre propio que defines una vez y ejecutas cuantas veces quieras. Y aquí va la buena noticia: llevas usándolas desde el primer día — print(), len(), input(), enumerate() son funciones que ya venían hechas. En esta lección aprenderás a crear las tuyas con def, a devolver resultados con return, a documentarlas con docstrings y a entender dónde "viven" sus variables. Como proyecto, empezaremos la reorganización de menu.py extrayendo sus dos primeras piezas: mostrar_catalogo() y buscar_libro().
Contenido
- Qué es una función y por qué usarla
- Definir con
defy llamar a la función - Parámetros: datos de entrada
return: devolver resultados (y el casoNone)- Docstrings: documentar la función
- Ámbito de las variables: local frente a global
- Proyecto: primera refactorización de
menu.py
Qué es una función y por qué usarla
Una función es un bloque de código con nombre, que opcionalmente recibe datos de entrada (parámetros) y opcionalmente devuelve un resultado. Piensa en la fotocopiadora de Papyrus: Ana no reconstruye la máquina cada vez que necesita una copia; mete un original (entrada), pulsa un botón (llamada) y recoge la copia (resultado).
Las tres razones clásicas para usar funciones:
| Motivo | Sin funciones | Con funciones |
|---|---|---|
| Reutilización | El cálculo del precio de socio está copiado en 3 scripts | Se escribe una vez y se llama desde donde haga falta |
| Mantenimiento | Cambiar el IVA obliga a editar cada copia (y olvidar una = bug) | Se cambia en un único sitio |
| Legibilidad | menu.py es un bloque de 35 líneas que hay que leer entero |
mostrar_catalogo() se explica sola por su nombre |
Definir con def y llamar a la función
La sintaxis mínima:
Desglose línea a línea:
def(de define) inicia la definición.saludares el nombre: mismas reglas que las variables (snake_case, PEP 8). Suele ser un verbo, porque una función hace algo.- Los paréntesis
()contendrán los parámetros (aquí, ninguno) y los dos puntos:abren el bloque. - El cuerpo va indentado, exactamente igual que en
ifofor. saludar()— con paréntesis — llama (ejecuta) la función. Sin paréntesis,saludarsolo nombra la función sin ejecutarla; es un error silencioso muy común.
Un detalle importante: definir no es ejecutar. Python lee el def, memoriza el bloque bajo ese nombre y sigue adelante. El cuerpo solo corre cuando alguien llama a la función. Por eso este script imprime en un orden que sorprende a los principiantes:
def despedir():
print("2. Dentro de la función")
print("1. Antes de llamar")
despedir()
print("3. Después de llamar")sequenceDiagram
participant S as Script principal
participant F as despedir()
S->>S: print("1. Antes de llamar")
S->>F: llamada despedir()
F->>F: print("2. Dentro de la función")
F-->>S: fin de la función (vuelve al punto de llamada)
S->>S: print("3. Después de llamar")
Parámetros: datos de entrada
Una función sin entradas siempre hace lo mismo. Los parámetros la hacen flexible:
def etiquetar(titulo, precio):
print(f"{titulo:<12} {precio:>6.2f} €")
etiquetar("Hamlet", 9.95)
etiquetar("Fausto", 21.00)tituloyprecioson parámetros: nombres que la función usa internamente."Hamlet"y9.95son argumentos: los valores concretos que envías en la llamada. El primero va al primer parámetro, el segundo al segundo (por posición).- En cada llamada, los parámetros nacen con los valores recibidos, la función trabaja y al terminar desaparecen.
De momento nos basta con esta forma básica; la lección 03-02 está dedicada por completo a los tipos de argumentos (con nombre, valores por defecto, cantidades variables...).
return: devolver resultados (y el caso None)
print() muestra, pero no entrega. Si quieres usar el resultado de una función en un cálculo posterior, necesitas return:
def precio_socio(base):
"""Precio para socios: descuento del 5% y luego IVA del 4%."""
final = base * (1 - 0.05) * (1 + 0.04)
return round(final, 2)
precio_hamlet = precio_socio(9.95)
print(f"Luis paga {precio_hamlet} €") # Luis paga 9.83 €
print(f"Dos ejemplares: {precio_socio(9.95) * 2} €")Claves de return:
- Entrega un valor al punto de llamada: la expresión
precio_socio(9.95)vale9.83, y puedes asignarla, imprimirla u operarla. - Termina la función inmediatamente: el código posterior al
returnejecutado no corre nunca. Esto lo convierte en la salida de emergencia perfecta — recuerda el consejo del módulo 2: dondebreaksolo sale del bucle más interno,returnsale de la función entera, bucles anidados incluidos. - Una función puede tener varios
return(por ejemplo, uno dentro de unify otro al final); en cada llamada se ejecuta como mucho uno.
¿Y si una función no tiene return, o escribe return a secas? Devuelve None, el "valor vacío" que conociste en el módulo 1:
def avisar_agotado(titulo):
print(f"'{titulo}' está agotado.")
resultado = avisar_agotado("Fausto")
print(resultado) # NoneLa tabla que evita el 90 % de las confusiones iniciales:
print(x) dentro de la función |
return x |
|
|---|---|---|
| ¿Muestra algo en pantalla? | Sí | No (salvo que quien llama lo imprima) |
| ¿La llamada tiene un valor útil? | No (vale None) |
Sí (vale x) |
| ¿Se puede seguir operando con el resultado? | No | Sí |
| Uso típico | Informar al usuario | Calcular para otro código |
Regla práctica para Papyrus: las funciones que calculan (precios, búsquedas) deben devolver con return; las que presentan (mostrar el catálogo) pueden limitarse a print() y devolver None.
Docstrings: documentar la función
La primera línea del cuerpo, si es una cadena, se convierte en la docstring: la documentación oficial de la función. Se escribe entre triples comillas:
def precio_socio(base):
"""Devuelve el precio final para socios de Papyrus.
Aplica el descuento de socio (5%) y después el IVA de libros (4%),
redondeando a 2 decimales.
"""
return round(base * (1 - 0.05) * (1 + 0.04), 2)
help(precio_socio) # muestra la docstring en el REPL- No es un comentario decorativo:
help(), VS Code (al pasar el ratón sobre la llamada) y las herramientas de documentación la leen. - Convención: primera línea en forma de resumen que quepa en una línea; si hace falta más detalle, línea en blanco y párrafo explicativo.
- Los comentarios
#explican cómo funciona un fragmento por dentro; la docstring explica qué hace la función y cómo usarla desde fuera.
Ámbito de las variables: local frente a global
¿Dónde "vive" una variable? Depende de dónde se asigne:
NOMBRE_TIENDA = "Papyrus" # ámbito global (nivel del script)
def crear_ticket():
linea = f"Ticket de {NOMBRE_TIENDA}" # linea: ámbito LOCAL
return linea
print(crear_ticket()) # Ticket de Papyrus
print(linea) # NameError: name 'linea' is not definedlineaes local: nace al llamar a la función y muere al terminar. Fuera, no existe. Esto es una ventaja enorme: dos funciones pueden usarlinea,iototalsin pisarse.NOMBRE_TIENDAes global: se asignó en el nivel del script, y las funciones pueden leerla sin hacer nada especial. Python, al no encontrarNOMBRE_TIENDAen el ámbito local, la busca "un piso más arriba".
El matiz delicado llega al intentar modificar una global dentro de una función:
Al ver la asignación caja = ..., Python decide que caja es local a cobrar... y entonces caja + importe intenta leer una local que aún no tiene valor. Existe una palabra clave que lo "arregla":
global caja le dice a Python "cuando asigne caja, usa la global". Funciona, pero conviene evitarla: una función que modifica globales tiene efectos invisibles desde la llamada — leyendo cobrar(12.35) nadie sospecha que caja cambió, y con varias funciones tocando las mismas globales el programa se vuelve indepurable. La alternativa limpia es recibir y devolver:
def cobrar(caja, importe):
"""Devuelve el nuevo total de caja tras cobrar el importe."""
return caja + importe
caja = 0.0
caja = cobrar(caja, 12.35) # el cambio se VE en el punto de llamadaResumen de reglas de ámbito:
| Operación dentro de la función | ¿Necesita algo especial? | ¿Recomendable? |
|---|---|---|
Leer una global (constantes como IVA_LIBROS) |
No | Sí — es el uso natural de las constantes |
| Asignar una variable nueva | No (será local) | Sí |
| Modificar una global | Sí, global nombre |
No — recibe el dato como parámetro y devuelve el resultado |
Proyecto: primera refactorización de menu.py
Cumplamos la promesa del módulo 2. Refactorizar es reorganizar código sin cambiar lo que hace, y menu.py lo pide a gritos: extraeremos la opción 1 a mostrar_catalogo() y el corazón de la opción 2 a buscar_libro(). En la carpeta papyrus (con el .venv activado), la nueva versión:
NOMBRE_TIENDA = "Papyrus"
catalogo = ["La Odisea", "Hamlet", "El Quijote"]
precios = [12.50, 9.95, 15.90]
stocks = [4, 6, 8]
def mostrar_catalogo():
"""Imprime la tabla de títulos, precios y stock."""
print(f"\n{'Título':<12} {'Precio':>8} {'Stock':>6}")
print("-" * 28)
for i, titulo in enumerate(catalogo):
print(f"{titulo:<12} {precios[i]:>6.2f} € {stocks[i]:>6}")
def buscar_libro(buscado):
"""Devuelve el índice del libro cuyo título coincide, o None si no está."""
for i, titulo in enumerate(catalogo):
if titulo.lower() == buscado.lower():
return i
return None
print(f"Bienvenida a la gestión de {NOMBRE_TIENDA}")
while True:
print()
print("1. Ver catálogo")
print("2. Buscar un libro")
print("3. Salir")
opcion = input("Elige una opción (1-3): ")
match opcion:
case "1":
mostrar_catalogo()
case "2":
buscado = input("Título a buscar: ")
if not buscado:
print("No has escrito nada.")
continue
indice = buscar_libro(buscado)
if indice is None:
print(f"'{buscado}' no está en el catálogo.")
else:
print(f"'{catalogo[indice]}': {precios[indice]:.2f} €, stock {stocks[indice]}.")
case "3":
print(f"Cerrando {NOMBRE_TIENDA}. ¡Hasta mañana, Ana!")
break
case _:
print("Opción no válida. Elige 1, 2 o 3.")Fíjate en las decisiones de diseño:
mostrar_catalogo()presenta: imprime y devuelveNone; elwhilequeda legible — la opción 1 es ahora una sola línea que se lee como una frase.buscar_libro()calcula: sustituye el patrónfor/break/elsede la versión anterior por algo aún más directo —return icorta la búsqueda en cuanto hay coincidencia (hace el papel delbreak) y elreturn Nonefinal cubre el caso "no encontrado" (el papel delelsedel bucle). Devolver el índice en lugar de imprimir permite que quien llama decida qué hacer con él; nos lo agradeceremos en la lección 03-04.if indice is Nonedistingue "no encontrado" de un resultado válido; ojo,if not indicesería un bug, porque el índice0(La Odisea) también es falsy — truthiness del módulo 2 aplicada.- Las funciones leen las globales
catalogo,preciosystockspero no las modifican: uso legítimo del ámbito global. Cuando extraigamos estas funciones a un módulo propio (03-04) verás cómo eliminar también esa dependencia.
El comportamiento del menú es idéntico al del módulo 2 — esa es la gracia de refactorizar — pero ahora la búsqueda es reutilizable: ticket.py o venta_socio.py podrán usar buscar_libro() en cuanto sepamos compartir código entre archivos (lección 03-04).
Errores Comunes y Consejos
- Llamar sin paréntesis:
mostrar_catalogo(sin()) no ejecuta nada ni da error; simplemente nombra la función. Si "la función no hace nada", revisa los paréntesis. - Usar la función antes de definirla: Python lee el archivo de arriba abajo; llamar a
buscar_libro()en una línea anterior a sudefproduceNameError. Convención práctica: constantes y datos arriba, luego losdef, y el programa principal al final. - Confundir
printconreturn: sitotal = precio_socio(9.95)te daNone, casi seguro que la función imprime en lugar de devolver. Repasa la tabla comparativa de esta lección. - Código después del
return: cualquier línea tras unreturnque siempre se ejecuta es código muerto. VS Code lo atenúa visualmente; bórralo o replantea la lógica. - Abusar de
global: si te descubres escribiendoglobalen varias funciones, el diseño cojea. Pasa los datos como parámetros y devuelve los resultados. - Olvidar la docstring: para funciones de proyecto (las que otros —o tú dentro de un mes— reutilizarán), una línea de docstring es el mínimo educado.
- Consejo: una función debe hacer una cosa. Si al describirla necesitas la palabra "y" ("muestra el catálogo y pide un libro y lo busca"), probablemente son dos o tres funciones.
Ejercicios
Ejercicio 1: ficha con y sin return
Escribe dos funciones: hay_stock(unidades), que reciba un número de unidades y devuelva True si es mayor que 0 y False en caso contrario; y mostrar_disponibilidad(titulo, unidades), que use la primera para imprimir '<titulo>': disponible o '<titulo>': agotado. Pruébalas con Hamlet (6 unidades) y Fausto (0 unidades). Añade docstring a ambas.
Ejercicio 2: contar agotados sin tocar globales
Dada la lista global stocks = [4, 6, 8, 0], escribe contar_agotados(lista_stocks) que reciba una lista de stocks por parámetro y devuelva cuántos valen 0 (patrón contador del módulo 2). Llámala con stocks e imprime Libros agotados: 1. Comprueba que la función funciona igual con [0, 0, 3]. ¿Por qué es mejor recibir la lista como parámetro que leer la global directamente?
Ejercicio 3: el detective del ámbito
Sin ejecutarlo, predice qué imprime este script y por qué. Después ejecútalo y comprueba tu hipótesis:
mensaje = "caja cerrada"
def abrir_caja():
mensaje = "caja abierta"
return mensaje
print(abrir_caja())
print(mensaje)Soluciones
Ejercicio 1:
def hay_stock(unidades):
"""Devuelve True si quedan unidades disponibles."""
return unidades > 0
def mostrar_disponibilidad(titulo, unidades):
"""Imprime si un título está disponible o agotado."""
if hay_stock(unidades):
print(f"'{titulo}': disponible")
else:
print(f"'{titulo}': agotado")
mostrar_disponibilidad("Hamlet", 6) # 'Hamlet': disponible
mostrar_disponibilidad("Fausto", 0) # 'Fausto': agotadoObserva que return unidades > 0 devuelve directamente el booleano de la comparación: no hace falta if unidades > 0: return True else: return False. Y una función puede llamar a otra: es la base de construir programas por capas.
Ejercicio 2:
stocks = [4, 6, 8, 0]
def contar_agotados(lista_stocks):
"""Devuelve cuántos elementos de la lista valen 0."""
agotados = 0
for unidades in lista_stocks:
if unidades == 0:
agotados += 1
return agotados
print(f"Libros agotados: {contar_agotados(stocks)}") # 1
print(contar_agotados([0, 0, 3])) # 2Recibir la lista como parámetro hace la función reutilizable con cualquier lista (la de Papyrus hoy, la de otra tienda mañana, una lista de prueba en un test) y deja claro en la llamada qué datos usa. La variable agotados es local: no ensucia el script.
Ejercicio 3: Imprime caja abierta y después caja cerrada. La asignación mensaje = "caja abierta" dentro de la función crea una variable local que oculta a la global mientras la función corre; la global nunca cambia. Para que la función modificara la global haría falta global mensaje — que, como hemos visto, es mejor evitar devolviendo el valor (justo lo que hace el return).
Conclusión
Ya sabes crear funciones con def, distinguir parámetros (entrada) de return (salida), documentarlas con docstrings y razonar sobre el ámbito de sus variables: locales que nacen y mueren con cada llamada, globales que se pueden leer (constantes) pero que no conviene modificar con global. Con ello, menu.py ha dado su primer paso de "script suelto" a "programa organizado": mostrar_catalogo() y buscar_libro() tienen nombre, docstring y una única responsabilidad. Pero nuestras funciones aún son rígidas: buscar_libro() recibe un único argumento posicional y no hay manera de darle opciones. En la siguiente lección exploraremos a fondo los argumentos de función — con nombre, valores por defecto, cantidades variables con *args y **kwargs — y saldaremos una deuda pendiente del módulo 2: entender de verdad zip(), la herramienta que recorre en paralelo catalogo, precios y stocks.
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
