Módulos y paquetes en Python — práctica con un boletín, un módulo que se prueba solo y un paquete (y el assert que tumbaba mi programa al importar)
Escribí unas pruebas con assert para mi módulo de validación y las dejé al final del archivo, sin más. Funcionaron de maravilla… hasta el día que cambié una constante del módulo y una de esas pruebas dejó de cumplirse. Entonces no fue el módulo el que protestó: fue toda mi aplicación, que se cayó con un AssertionError nada más arrancar, antes de llegar a ninguna de sus propias líneas. Mis pruebas se ejecutaban cada vez que alguien importaba el módulo.
Lo que aprendí de golpe fue que escribir un módulo es una cosa, pero decidir qué debe pasar al importarlo y qué al lanzarlo es otra. Hoy lo practicamos con tres programas completos: un módulo propio con el programa que lo importa, un módulo que se prueba a sí mismo y un paquete con subpaquete.
Esta práctica usa lo de la teoría de módulos y paquetes: import, from … import, __name__ y paquetes. Si vienes de las funciones en Python, aquí las vas a reutilizar desde varios archivos.
Tabla de Contenidos
Cómo usar este artículo
Cada programa se compone de varios archivos que van en la misma carpeta (y, en el último, en una estructura de carpetas). Créalos tú mismo, copia el código y lánzalo desde la terminal con python3 nombre.py. Si usas un editor con botón de ejecución, comprueba que ejecuta desde la carpeta del proyecto; si no, los import no encontrarán tus módulos.
Ningún programa usa input(). Los datos van fijos en el código para que tu salida sea idéntica a la que ves aquí.
Programa 1 · Un módulo propio y el programa que lo importa
Un boletín de notas necesita media y calificación escrita. Lo separamos en dos archivos: boletin.py, que es la librería, e informe.py, que es el programa que la usa.
boletin.py
"""Utilidades para el boletín de notas."""
NOTA_MAX = 10
APROBADO = 5.0
NOTABLE = 7.0
SOBRESALIENTE = 9.0
def media(a, b, c):
"""Media aritmética de tres notas."""
return (a + b + c) / 3
def califica(nota):
"""Convierte una nota numérica en su calificación escrita."""
if nota >= SOBRESALIENTE:
return "Sobresaliente"
if nota >= NOTABLE:
return "Notable"
if nota >= APROBADO:
return "Aprobado"
return "Suspenso"
informe.py
import boletin as bol
from boletin import media, califica
print("=== Boletín de Lucía (sobre", bol.NOTA_MAX, ") ===")
fp1 = media(7.0, 8.5, 9.5)
algebra = media(4.0, 5.5, 3.5)
ic1 = media(6.5, 5.0, 7.5)
print("FP1: ", round(fp1, 2), "->", califica(fp1))
print("Álgebra:", round(algebra, 2), "->", califica(algebra))
print("IC1: ", round(ic1, 2), "->", califica(ic1))
if bol.APROBADO > algebra:
print("Aviso: Álgebra está por debajo de", bol.APROBADO)
Se lanza con python3 informe.py:
=== Boletín de Lucía (sobre 10 ) === FP1: 8.33 -> Notable Álgebra: 4.33 -> Suspenso IC1: 6.33 -> Aprobado Aviso: Álgebra está por debajo de 5.0
Entre líneas:
boletin.pysolo define cosas: constantes y funciones. Si lo lanzas solo, no imprime nada, y eso es justo lo que se espera de una librería.- En
informe.pyusamos las dos formas a la vez:from boletin import media, calificapara las funciones que usamos mucho, eimport boletin as bolpara las constantes, con un alias corto. - ¿Por qué
bol.APROBADOy no soloAPROBADO? Porque no la importamos confrom. Si la hubiéramos traído confrom boletin import APROBADO, también valdría, pero así se lee de dónde sale el valor. - Fíjate en el orden de las comprobaciones en
califica: va de la nota más alta a la más baja. Cuando entra en unif, ya no sigue mirando los demás, porquereturntermina la función. - ¿Y si el umbral de
Notablecambiara? Basta con tocarlo enboletin.py, y cualquier programa que use el módulo se actualiza solo. Esa es la gracia de modularizar.
Pruébalo tú
- Añade a
boletin.pyuna funciónaprobado(nota)que devuelvaTrueoFalse, y úsala eninforme.pyen vez de comparar conbol.APROBADO. - Añade una cuarta asignatura al boletín. ¿Cuántas líneas has tenido que tocar en
boletin.py? (Pista: ninguna.)
Programa 2 · Un módulo que se prueba a sí mismo
Hay módulos que son librería y, a la vez, se pueden lanzar para comprobar que funcionan. Es el truco de if __name__ == "__main__":. Aquí tenemos un módulo para validar notas, con sus pruebas dentro, y una aplicación que lo usa.
validacion.py
"""Validación de notas."""
NOTA_MIN = 0
NOTA_MAX = 10
print("validacion.py: __name__ =", __name__)
def es_nota_valida(nota):
return NOTA_MIN <= nota <= NOTA_MAX
def limita(nota):
"""Recorta la nota al rango válido."""
return max(NOTA_MIN, min(nota, NOTA_MAX))
if __name__ == "__main__":
# Pruebas: solo se ejecutan si lanzas este archivo directamente
print("Pruebas de validacion.py")
assert es_nota_valida(7.5)
assert not es_nota_valida(11)
assert limita(12) == 10
assert limita(-3) == 0
assert limita(6.5) == 6.5
print("Todo correcto")
app.py
from validacion import es_nota_valida, limita
nota = 11.5
print("¿Válida?", es_nota_valida(nota))
print("Nota corregida:", limita(nota))
Primero lanzamos la aplicación, con python3 app.py:
validacion.py: __name__ = validacion ¿Válida? False Nota corregida: 10
Y ahora el módulo, directamente, con python3 validacion.py:
validacion.py: __name__ = __main__ Pruebas de validacion.py Todo correcto
Entre líneas:
- La primera línea que ves en las dos salidas viene del
printque hemos dejado a propósito en el nivel superior del módulo. Fíjate en el valor de__name__:validacioncuando lo importaapp.py,__main__cuando lo lanzas tú. - Las pruebas no aparecen cuando se lanza
app.py: están bajo elif. En el segundo caso se ejecutan, y como todas se cumplen, solo ves «Todo correcto». assert condicionno hace nada si la condición es verdadera, y detiene el programa con unAssertionErrorsi es falsa. Es una forma rapidísima de dejar escritas las pruebas.limitausaminymaxpara recortar al rango:min(nota, NOTA_MAX)evita pasarse por arriba ymax(NOTA_MIN, ...)por abajo. Si no lo ves claro, pruébalo con12y con-3.- ¿Y si borrases las pruebas del
ify las dejaras sueltas? Volverías a mi historia del principio: cualquier programa que importara el módulo las ejecutaría y, si una fallase, se caería antes de empezar.
Pruébalo tú
- Rompe una prueba a propósito (por ejemplo,
assert limita(12) == 11) y lanzapython3 validacion.py. Lee el mensaje deAssertionError. Lanza ahorapython3 app.py: ¿sigue funcionando? - Añade una función
redondea(nota)que redondee a un decimal y su prueba. ¿Dónde la escribes: encima o dentro delif?
Programa 3 · Un paquete con subpaquete
Ahora organizamos un expediente académico en un paquete. Tiene un módulo con los créditos del grado, y un subpaquete notas con dos módulos: uno que calcula y otro que da formato de texto.
main.py
expediente/
├── __init__.py
├── creditos.py
└── notas/
├── __init__.py
├── calculos.py
└── texto.py
Los dos __init__.py están vacíos. Estos son los demás archivos:
expediente/creditos.py
CREDITOS_GRADO = 240
def porcentaje_superado(creditos):
"""Porcentaje del grado superado (1 decimal)."""
return round(creditos / CREDITOS_GRADO * 100, 1)
expediente/notas/calculos.py
def media_ponderada(nota1, cred1, nota2, cred2):
"""Media de dos asignaturas ponderada por sus créditos."""
return (nota1 * cred1 + nota2 * cred2) / (cred1 + cred2)
expediente/notas/texto.py
from expediente.notas.calculos import media_ponderada
def resumen(nota1, cred1, nota2, cred2):
media = media_ponderada(nota1, cred1, nota2, cred2)
return f"Media ponderada: {media:.2f}"
main.py
import expediente.creditos
from expediente.notas.calculos import media_ponderada
from expediente.notas import texto
print("Grado superado:", expediente.creditos.porcentaje_superado(60), "%")
print("Media ponderada:", media_ponderada(9.0, 6, 6.0, 6))
print(texto.resumen(9.0, 6, 6.0, 6))
Se lanza con python3 main.py, desde la carpeta que contiene expediente:
Grado superado: 25.0 % Media ponderada: 7.5 Media ponderada: 7.50
Entre líneas:
texto.pyusa la ruta completa para llegar acalculos.py:from expediente.notas.calculos import media_ponderada. Las rutas se escriben siempre desde la carpeta de arriba, que es donde estámain.py.main.pyusa las tres formas posibles:import expediente.creditos(ruta completa al usar),from expediente.notas.calculos import media_ponderada(función suelta) yfrom expediente.notas import texto(el módulo, sin la ruta completa).- Esta última suele pasar desapercibida:
from paquete import modulotambién es válido y deja usarmodulo.función. Es un término medio entre importar la función y escribir la ruta entera. - Los números cuadran: la media ponderada de un 9,0 (6 créditos) y un 6,0 (6 créditos) es 7,5, y 60 de 240 créditos son el 25,0 % del grado.
f"{media:.2f}"formatea con dos decimales, por esotexto.resumenda7.50ymedia_ponderadada7.5.
Pruébalo tú
- Añade
expediente/notas/escala.pycon una función que convierta una nota de 0–10 a una escala de 0–4 y úsala desdemain.py. ¿Qué línea de importación necesitas? - Borra un
__init__.pyy vuelve a lanzar el programa. ¿Falla? Lee la 🟣 nota de la teoría y razona por qué puede que no.
Míralo paso a paso en Python Tutor
Como Python Tutor solo admite un archivo, juntamos en uno el módulo del Programa 1 y el programa que lo usa. Elige Python 3 y pega el código. Verás cómo la pila de llamadas crece y se vacía cuando unas funciones llaman a otras, que es lo que ocurre entre tus módulos aunque estén en archivos distintos.
APROBADO = 5.0
def media(a, b, c):
return (a + b + c) / 3
def califica(nota):
if nota >= 9:
return "Sobresaliente"
if nota >= APROBADO:
return "Aprobado"
return "Suspenso"
def informe(a, b, c):
m = media(a, b, c)
return califica(m)
if __name__ == "__main__":
print(informe(9.0, 9.5, 8.5))
print(informe(4.0, 5.0, 3.5))
Salida esperada:
Sobresaliente Suspenso
Qué observar, paso a paso:
- Al empezar, solo existen
APROBADOy las tres funciones en las variables globales. Ninguna se ha ejecutado todavía:defsolo las crea. - Cuando entra en el bloque
if __name__ == "__main__":, Python Tutor lo trata como programa principal. Es el mismo bloque que envalidacion.pyse saltaría si el archivo fuera importado. - La llamada
informe(9.0, 9.5, 8.5)abre un marco nuevo. Dentro,media(a, b, c)abre otro encima: ves la pila con dos marcos, ymguarda9.0cuando el segundo se cierra. califica(m)abre otro marco, devuelve"Sobresaliente"y se cierra. Luego se cierrainforme. Entre archivos pasa exactamente lo mismo.- En la segunda llamada, fíjate en que
mvale4.166…y la función entra por el últimoreturn: la nota es menor queAPROBADO.
Para cerrar
Hoy has visto los tres patrones que usarás siempre: una librería que solo define, un módulo autoprobado gracias al __main__, y un paquete con subpaquete donde cada pieza tiene su sitio. Si algo no te sale, repasa la teoría; y cuando te sientas cómodo, pasa a los ejercicios, donde los fallos vienen incluidos.
¿Te ha servido este artículo? Así puedes apoyar SergioLearns →
