Módulos y paquetes en Python — teoría con utilidades de notas, import y __main__ (y el programa que imprimía cosas que yo no había pedido)

Había escrito un archivo notas.py con mis funciones y, al final, unas líneas para probarlas mientras lo escribía. Todo bien. Días después, en otro archivo, escribí import notas para reutilizar la función media, ejecuté mi programa nuevo y esto fue lo que salió:

Probando media...
7.0
Mi programa empieza aquí
7.0

«Yo solo había pedido la media de 5, 7 y 9… ¿de dónde sale ese «Probando media…» de antes de empezar?» Estuve un rato mirando mi programa nuevo, que no tenía ni rastro de esas líneas. Lo que pasa es que import ejecuta el archivo entero, y yo había dejado mis pruebas sueltas al final del módulo.

Hoy lo ordenamos todo. Verás dónde conviene escribir tus funciones, qué es un módulo, cómo se usa con import y con from … import, por qué existe el prefijo (los espacios de nombres), qué significa de verdad if __name__ == "__main__": y cómo se agrupan varios módulos en un paquete. Todo con un mismo ejemplo: unas utilidades para trabajar con notas. Si vienes de funciones en Python, esto es el siguiente paso natural: ya sabes escribir funciones; ahora aprenderás a guardarlas donde puedan reutilizarse. Y cuando termines, la práctica de módulos y paquetes te espera con tres programas completos.

¿Dónde escribo mis funciones?

Hasta ahora has escrito las funciones en el mismo archivo que el programa que las usa. Para programas pequeños es lo normal y funciona perfectamente:

NOTA_MAX = 10
APROBADO = 5.0


def media(a, b, c):
    """Media de tres notas."""
    return (a + b + c) / 3


def esta_aprobado(nota):
    return nota >= APROBADO


m = media(6.0, 7.5, 7.5)
print("Media:", m)
print("¿Aprobado?", esta_aprobado(m))
Media: 7.0
¿Aprobado? True

El problema llega cuando el programa crece. Imagina que mañana necesitas media en otro programa distinto: tendrías que copiar y pegar la función. Y si luego corriges un fallo en una copia, la otra sigue teniéndolo. Copiar código es la forma más rápida de tener el mismo error en dos sitios.

Python te da otra opción: escribir las funciones en otro archivo y pedirle a tu programa que las use. Eso es la programación modular: separar un programa en partes independientes, cada una con su funcionalidad, que puedes reutilizar e intercambiar. En Python, esas partes se llaman módulos.

Un módulo es un archivo .py

Un módulo es simplemente un archivo .py, y su nombre es el del archivo sin la extensión. Si guardamos las funciones y las constantes en notas.py…

notas.py

NOTA_MAX = 10
APROBADO = 5.0


def media(a, b, c):
    """Media de tres notas."""
    return (a + b + c) / 3


def esta_aprobado(nota):
    return nota >= APROBADO

…cualquier otro archivo de la misma carpeta puede usarlas con import:

programa.py

import notas

m = notas.media(6.0, 7.5, 7.5)
print("Media:", m)
print("¿Aprobado?", notas.esta_aprobado(m))
print("Nota máxima posible:", notas.NOTA_MAX)
Media: 7.0
¿Aprobado? True
Nota máxima posible: 10

Entre líneas:

  • import notas busca un archivo llamado notas.py y lo carga. Escribimos notas, sin .py.
  • Para usar algo del módulo, lo escribes con el prefijo: notas.media(...), notas.NOTA_MAX. Sirve para funciones y también para variables y constantes.
  • programa.py no contiene ni una línea de media, y sin embargo la usa. Si mañana mejoras media en notas.py, todos los programas que la importan se benefician.

Olvidar el prefijo. Con import notas, el nombre media por sí solo no existe en tu programa: existe notas.media.

Traceback (most recent call last):
  File "error_prefijo.py", line 3, in <module>
    m = media(6.0, 7.5, 7.5)
        ^^^^^
NameError: name 'media' is not defined

Ponerle a tu módulo el nombre de uno que Python ya trae (math.py, random.py, string.py…). Cuando hagas import math en ese mismo proyecto, Python puede cargar tu archivo en lugar del suyo, y tendrás errores muy raros. Elige nombres propios y que describan lo que hace el módulo.

Espacios de nombres: por qué hace falta el prefijo

¿Por qué no me deja usar media a secas? Porque cada módulo es un espacio de nombres: una caja con sus propios nombres. Lo que se define dentro de notas.py vive en la caja notas, y por eso necesitas decir de qué caja lo sacas.

Esto sirve para que dos módulos puedan tener cosas con el mismo nombre sin molestarse. Imagina que, además de notas.py, tenemos asistencia.py, que también tiene una función llamada media, pero para calcular la asistencia media en porcentaje:

notas.py

def media(a, b, c):
    """Media de tres notas."""
    return (a + b + c) / 3

asistencia.py

def media(p1, p2):
    """Asistencia media (en %) de dos trimestres, redondeada."""
    return round((p1 + p2) / 2)

Las dos se llaman media, pero no hay conflicto porque viven en cajas distintas:

import notas
import asistencia

print("Media de notas:", notas.media(6.0, 7.5, 7.5))
print("Asistencia media:", asistencia.media(100, 70), "%")
Media de notas: 7.0
Asistencia media: 85 %

notas.media y asistencia.media son funciones distintas que casualmente comparten nombre. Gracias al prefijo, sabes siempre cuál estás llamando.

from … import y los alias

Escribir notas. delante cada vez puede resultar pesado. Python ofrece dos atajos:

from notas import media, esta_aprobado    # nombres sueltos, sin prefijo
import notas as nt                        # alias corto para el módulo
from notas import NOTA_MAX as MAXIMA      # alias para un solo nombre

m = media(6.0, 7.5, 7.5)
print("Media:", m)
print("¿Aprobado?", esta_aprobado(m))
print("Nota máxima:", nt.NOTA_MAX, MAXIMA)
print("¿La misma función?", media is nt.media)
Media: 7.0
¿Aprobado? True
Nota máxima: 10 10
¿La misma función? True
FormaCómo se usa despuésCuándo conviene
import notasnotas.media(...)Por defecto: se ve de dónde viene cada cosa
from notas import mediamedia(...)Cuando usas poco el módulo y ningún nombre se va a repetir
import notas as ntnt.media(...)Módulos con nombre largo
from notas import media as media_notasmedia_notas(...)Dos módulos con un nombre repetido

Fíjate en la última línea de la salida: media is nt.media da True. Con from … import no copias la función: le pones otra etiqueta a la misma función. Los alias funcionan igual.

Hacer from … import de dos módulos que tienen un nombre repetido. Con from, el nombre entra directamente en tu programa sin prefijo, así que el segundo import pisa al primero:

pisado.py

from notas import media
from asistencia import media     # ¡pisa a la anterior!

print(media(6.0, 7.5, 7.5))
Traceback (most recent call last):
  File "pisado.py", line 4, in <module>
    print(media(6.0, 7.5, 7.5))
          ^^^^^^^^^^^^^^^^^^^^
TypeError: media() takes 2 positional arguments but 3 were given

Aquí media ya no es la de notas, sino la de asistencia (la segunda importada), que recibe dos parámetros en vez de tres. Y lo peor: si ambas hubieran recibido el mismo número de parámetros, no habría error: simplemente saldrían resultados equivocados.

Cuando dos módulos comparten un nombre, usa alias (from asistencia import media as media_asistencia) o importa el módulo entero y usa el prefijo. Y evita from modulo import *: mete todos los nombres del módulo de golpe y no sabrás de dónde viene cada uno.

El módulo principal y el código suelto

Volvamos al misterio del principio. Cuando Python importa un módulo, ejecuta el archivo de arriba abajo, la primera vez que se importa. Definir funciones y constantes es inofensivo, pero cualquier instrucción suelta (un print, una prueba, una llamada) también se ejecuta. Mi notas.py de aquella historia era así:

notas.py (con pruebas sueltas)

NOTA_MAX = 10


def media(a, b, c):
    return (a + b + c) / 3


# Prueba rápida mientras escribía el módulo
print("Probando media...")
print(media(6.0, 7.5, 7.5))

Lo que necesitamos es que esas pruebas se ejecuten cuando **yo lance notas.py directamente**, pero no cuando otro programa lo importe. Python lo permite porque cada módulo tiene una variable especial, __name__:

  • Si lanzas el archivo directamente (python3 notas.py), Python le pone __name__ igual a "__main__": es el módulo principal, el que arranca todo.
  • Si el archivo se carga con import, __name__ vale el nombre del módulo ("notas").

Así que basta con meter las pruebas dentro de if __name__ == "__main__":. Para verlo, el módulo de abajo también imprime __name__ en cada caso:

notas.py (corregido)

NOTA_MAX = 10

print("__name__ vale:", __name__)


def media(a, b, c):
    return (a + b + c) / 3


if __name__ == "__main__":
    # Solo se ejecuta si lanzas ESTE archivo directamente
    print("Probando media...")
    print(media(6.0, 7.5, 7.5))

Si lo importamos desde programa.py (el mismo de antes), las pruebas ya no se cuelan:

__name__ vale: notas
Mi programa empieza aquí
7.0

Y si lanzamos notas.py directamente, las pruebas sí se ejecutan:

__name__ vale: __main__
Probando media...
7.0
Cómo se ejecutaValor de __name__¿Entra en el if?
python3 notas.py"__main__"Sí
import notas desde otro archivo"notas"No

Un módulo se ejecuta solo la primera vez que se importa. Si lo importas desde tres archivos, el código suelto de su nivel superior se ejecutaría una sola vez. Pero lo correcto es no depender de eso: deja en el nivel superior solo definiciones, y lo demás, bajo if __name__ == "__main__":.

Paquetes: carpetas de módulos

Cuando tienes muchos módulos, conviene agruparlos. Un paquete es una carpeta con módulos dentro y un archivo especial llamado __init__.py (puede estar vacío). Es lo que le dice a Python: «esta carpeta es un paquete». Los paquetes pueden contener a su vez otros paquetes, los subpaquetes.

Nuestras utilidades crecen y hacemos un paquete universidad, con un subpaquete becas:

programa.py
universidad/
├── __init__.py
├── notas.py
├── asistencia.py
└── becas/
    ├── __init__.py
    └── calculo.py

El punto en el import se lee como una barra de carpetas: universidad.becas.calculo es el archivo universidad/becas/calculo.py. Estos son los dos módulos nuevos (notas.py y asistencia.py son los de antes):

universidad/becas/calculo.py

from universidad.notas import APROBADO


def puntuacion(nota, asistencia):
    """Puntuación de beca: 80 % nota y 20 % asistencia (sobre 10)."""
    if nota < APROBADO:
        return 0
    return round(0.8 * nota + 0.2 * asistencia / 10, 2)

Fíjate en que calculo.py importa algo de otro módulo del paquete con la ruta completa: from universidad.notas import APROBADO. Y este es el programa que lo usa todo:

programa.py

from universidad.notas import media
import universidad.asistencia
from universidad.becas.calculo import puntuacion

m = media(6.0, 7.5, 7.5)
a = universidad.asistencia.media(100, 70)
print("Media:", m, "| Asistencia:", a, "%")
print("Puntuación de beca:", puntuacion(m, a))
Media: 7.0 | Asistencia: 85 %
Puntuación de beca: 7.3

Mira las tres formas de importar:

  • from universidad.notas import media: trae la función suelta; la usamos sin prefijo.
  • import universidad.asistencia: trae el módulo; hay que escribir la ruta completa (universidad.asistencia.media) cada vez.
  • from universidad.becas.calculo import puntuacion: baja dos niveles (paquete, subpaquete, módulo) y coge una sola función.

En las versiones modernas de Python una carpeta también funciona como paquete sin __init__.py (se llaman paquetes de espacio de nombres). Pero la asignatura lo pide, y es lo recomendable: deja claro que la carpeta es un paquete y es el sitio donde, más adelante, puedes poner código de inicialización.

Equivocarte en la ruta o el nombre. Python no adivina: si el módulo no existe en esa ruta, protesta.

Traceback (most recent call last):
  File "error_ruta.py", line 1, in <module>
    import universidad.nota
ModuleNotFoundError: No module named 'universidad.nota'

Lanzar directamente un archivo que está dentro del paquete. Python busca los paquetes a partir de la carpeta del archivo que ejecutas, y desde dentro de becas/ ya no ve la carpeta universidad. Lanza siempre el programa desde fuera del paquete (programa.py), no los módulos de su interior.

Traceback (most recent call last):
  File "universidad/becas/calculo.py", line 1, in <module>
    from universidad.notas import APROBADO
ModuleNotFoundError: No module named 'universidad'

Míralo paso a paso en Python Tutor

Python Tutor ejecuta tu programa línea a línea y dibuja las variables y las funciones en memoria. Elige Python 3. Tiene una limitación: solo admite un archivo, así que no puede importar tus propios módulos. Pero podemos simular lo importante: que un nombre es una etiqueta que apunta a una función, y que from … import solo cambia a qué función apunta.

def media(a, b, c):
    return (a + b + c) / 3


def media_asistencia(p1, p2):
    return round((p1 + p2) / 2)


media_notas = media           # alias: otra etiqueta para la misma función
print(media_notas(6.0, 7.5, 7.5))

media = media_asistencia      # ahora "media" apunta a la otra función
print(media(100, 70))
print(media_notas(6.0, 7.5, 7.5))   # el alias sigue apuntando a la original

if __name__ == "__main__":
    print("Soy el programa principal")

Salida esperada:

7.0
85
7.0
Soy el programa principal

Qué observar, paso a paso:

  1. Después de definir las dos funciones, en la zona de variables globales aparecen media y media_asistencia, cada una con una flecha hacia su propia caja de función.
  2. En media_notas = media no se crea ninguna función nueva: aparece una segunda flecha hacia la misma caja. Es lo que hace un alias o un from … import … as ….
  3. En media = media_asistencia, la flecha de media salta a la otra caja. Es lo que hace el segundo from … import media del error de antes: el nombre se queda con la última.
  4. La última llamada, con media_notas, sigue dando 7.0: el alias conservó su flecha original. Por eso los alias son la salida a los conflictos de nombres.
  5. En el bloque final, Python Tutor ejecuta tu código como programa principal, así que la condición __name__ == "__main__" se cumple y entras en el if.

Resumen

ConceptoLo esencial
MóduloUn archivo .py; su nombre es el del archivo sin la extensión
import moduloSe usa con prefijo: modulo.funcion()
from modulo import fSe usa sin prefijo; cuidado con los nombres repetidos
Alias (as)Otra etiqueta para el módulo o la función
Espacio de nombresCada módulo es una caja: a.media y b.media no chocan
if __name__ == "__main__":Código que solo corre al lanzar el archivo directamente
PaqueteCarpeta con módulos y un __init__.py; se importa con puntos

Tres ideas para quedarte:

  1. Importar ejecuta el archivo. Deja solo definiciones al nivel superior y lo demás bajo if __name__ == "__main__":.
  2. El prefijo es una ventaja, no un incordio. Dice de dónde viene cada cosa y evita choques de nombres. Si lo quitas con from, hazlo sabiendo qué nombres entran.
  3. Un punto es una carpeta. universidad.becas.calculo es universidad/becas/calculo.py, y se ejecuta siempre desde fuera del paquete.

En la práctica de módulos y paquetes montarás un boletín con tu propio módulo, un módulo que se prueba a sí mismo y un paquete con subpaquete. Después tienes los ejercicios para comprobar si lo has pillado. Y en el siguiente tema entramos en las secuencias: listas y tuplas.

Siguiente paso: prácticaMó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)

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Sigue aprendiendo

3 comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *