Introducción a los Módulos
¿Qué es un módulo?
Un módulo es un archivo con extensión .py que contiene definiciones de funciones, clases y variables. Su propósito principle es agrupar funcionalidades reutilizables para que otros programas puedan importarlas y utilizarlas sin duplicar código. Esto permite estructurar proyectos de manera modular y escalable.
En Python existen distintas categorías de módulos:
- Archivos
.pyescritos en Python - Extensiones compiladas en C o C++ (bibliotecas compartidas o DLL)
- Carpetas que contienen un archivo
__init__.py(paquetes) - Módulos integrados en el intérprete, escritos en C
Ventajas de usar módulos:
- Organizar el código a nivel de archivos, facilitendo el mantenimiento
- Reutilizar funcionalidades sin duplicar código entre distintos archivos
- Aprovechar módulos de terceros para acelerar el desarrollo
Ejemplo de un módulo denominado operaciones.py:
# operaciones.py
print('Módulo operaciones cargado')
saldo = 5000
def consultar():
print('Saldo actual:', saldo)
def transferir():
print('Iniciando transferencia...')
consultar()
def modificar():
global saldo
saldo = 0
Importación con import
La sentencia import permite cargar un módulo completo. Python ejecuta el contenido del módulo solo la primera vez que se importa; las importaciones subsiguientes simplemente agregan una referencia al objeto ya cargado en memoria.
main.py
import operaciones import operaciones import operaciones
Salida esperada:
Módulo operaciones cargado
<p>Puede verificarse qué módulos están cargados consultando <code>sys.modules</code>, que es un diccionario que mapea nombres de módulos con sus objetos correspondientes.</p>
<p><strong>Proceso durante la primera importación:</strong></p>
<ol>
<li>Se crea un nuevo espacio de nombres para el módulo importado</li>
<li>Se ejecuta el código del módulo dentro de ese espacio de nombres</li>
<li>Se asocia el nombre del módulo con dicho espacio de nombres</li>
</ol>
<p>Cada módulo posee su propio espacio de nombres aislado. Esto significa que las variables globales definidas en un módulo no entran en conflicto con las del archivo que lo importa.</p>
<code># main.py
import operaciones
saldo = 100
print(operaciones.saldo)
# Salida:
# Módulo operaciones cargado
# 5000
</code>
<code># main.py
import operaciones
def consultar():
print('==========')
operaciones.consultar()
# Salida:
# Módulo operaciones cargado
# Saldo actual: 5000
</code>
<p><strong>Asignar un alias al módulo:</strong></p>
<code>import operaciones as ops
print(ops.saldo)
</code>
<p>Los alias resultan útiles para crear código extensible. Por ejemplo, si existen distintos controladores de base de datos:</p>
<code># postgres.py
def analizar():
print('Analizando con PostgreSQL')
# sqlite_db.py
def analizar():
print('Analizando con SQLite')
# app.py
motor = input('Seleccione motor: ')
if motor == 'postgres':
import postgres as db
elif motor == 'sqlite':
import sqlite_db as db
db.analizar()
</code>
<p>También es posible importar varios módulos en una sola línea:</p>
<code>import sys, os, re
</code>
<h2>Importación con <code>from ... import</code></h2>
<p>Esta sintaxis importa nombres específicos directamente al espacio de nombres actual, eliminando la necesidad de usar el prefijo del módulo.</p>
<code>from operaciones import consultar, transferir
</code>
<p><strong>Diferencia clave con <code>import</code>:</strong></p>
importrequiere usar el prefijo:operaciones.consultar()from ... importpermite usar el nombre directamente:consutar()
La desventaja de from ... import es que puede generar conflictos si existe un nombre igual en el archivo actual.
# main.py
from operaciones import consultar
saldo = 999
consultar()
# Salida:
# Módulo operaciones cargado
# Saldo actual: 5000
Si se redefine el nombre importado, la versión local sobrescribe la importación:
# main.py
from operaciones import consultar
def consultar():
print('==========')
consultar()
# Salida:
# Módulo operaciones cargado
# ==========
También se pueden asignar alias y importar múltiples nombres:
from operaciones import consultar as ver_saldo
from operaciones import consultar, transferir, saldo
Importación con asterisco (from ... import *)
Esta forma importa todos los nombres públicos del módulo (aquellos que no comienzan con guion bajo). No se recomienda su uso generalizado porque puede causar conflictos de nombres y reduce la legibilidad del código.
from operaciones import *
print(saldo)
consultar()
transferir()
modificar()
Para controlar qué nombres se exportan con *, se puede definir __all__ en el módulo:
# operaciones.py
__all__ = ['saldo', 'consultar']
Recarga de módulos
Dado que Python almacena los módulos cargados en sys.modules, los cambios realizados en el archivo fuente después de la importación no se reflejan automáticamente. Para pruebas interactivas, puede utilizarse importlib.reload():
import importlib
import mi_modulo
importlib.reload(mi_modulo)
Doble propósito de los archivos .py
Un archivo Python puede funcionar como:
- Script: se ejecuta directamente como programa principal
- Módulo: se importa desde otros archivos para reutilizar sus funciones
La variable integrada __name__ permite distinguir ambos casos:
- Al ejecutar como script:
__name__ == '__main__' - Al importar como módulo:
__name__ == 'nombre_del_modulo'
# fibonacci.py
def serie_fibonacci(n):
a, b = 0, 1
while b < n:
print(b, end=' ')
a, b = b, a + b
print()
def fibonacci_lista(n):
resultado = []
a, b = 0, 1
while b < n:
resultado.append(b)
a, b = b, a + b
return resultado
if __name__ == "__main__":
import sys
serie_fibonacci(int(sys.argv[1]))
Ruta de búsqueda de módulos
El orden de búsqueda de un módulo es el siguiente:
- Módulos ya cargados en memoria (
sys.modules) - Módulos integrados del intérprete
- Directorios listados en
sys.path
sys.path se inicializa con:
- El directorio del script en ejecución (o el directorio actual)
- La variable de entorno
PYTHONPATH - Los directorios predeterminados de la instalación
Es posible modificar sys.path en tiempo de ejecución:
import sys
sys.path.append('/ruta/personalizada')
sys.path.insert(0, '/ruta/prioritaria')
Importante: los nombres de módulos personalizados no deben coincidir con los módulos integrados de Python.
Compilación de archivos Python
El intérprete almacena versiones compiladas de los módulos en el directorio __pycache__/ con el formato modulo.version.pyc. Esto acelera la carga de módulos (no su ejecución). Los archivos .pyc son bytecode independiente de la plataforma pero dependientes de la versión de Python.
Se pueden generar archivos compilados para todos los módulos de un directorio:
python -m compileall /ruta/del/proyecto
Paquetes
¿Qué es un paquete?
Un paquete es un directorio que contiene un archivo __init__.py y permite organizar módulos relacionados bajo un espacio de nombres común, utilizando la notación con puntos.
mi_paquete/
├── __init__.py
├── modulo_a.py
├── modulo_b.py
└── sub_paquete/
├── __init__.py
└── modulo_c.py
En Python 3, la presencia de __init__.py no es estrictamente obligatoria para que el directorio sea reconocido como paquete, pero se recomienda incluirlo para compatibilidad y claridad.
Reglas de importación en paquetes:
- En las sentencias
importyfrom ... import, todo lo que está a la izquierda del punto debe ser un paquete - Importar un paquete ejecuta su
__init__.py - Los módulos homónimos en paquetes distintos no entran en conflicto
Uso de paquetes
Considere la siguiente estructura de ejemplo:
plataforma/
├── __init__.py
├── api/
│ ├── __init__.py
│ ├── consultas.py
│ └── versiones.py
├── comandos/
│ ├── __init__.py
│ └── gestion.py
└── datos/
├── __init__.py
└── modelos.py
# consultas.py
def obtener():
print('Obteniendo datos desde consultas.py')
# versiones.py
def crear_recurso(config):
print('Creando recurso:', config)
# gestion.py
def ejecutar():
print('Ejecutando gestión')
# modelos.py
def registrar(motor):
print('Registrando modelos en:', motor)
Importación con import:
import plataforma.datos.modelos
plataforma.datos.modelos.registrar('postgresql')
Importar solo el nombre del paquete no carga automáticamente todos los submódulos. Para habilitar el acceso a subpaquetes, es necesario incluir importaciones en __init__.py:
# plataforma/__init__.py
from . import comandos
# plataforma/comandos/__init__.py
from . import gestion
Importación con from ... import:
from plataforma.datos import modelos
modelos.registrar('postgresql')
from plataforma.datos.modelos import registrar
registrar('postgresql')
Importación con asterisco desde paquetes:
# plataforma/api/__init__.py
valor = 42
def inicializar():
print('API inicializada')
__all__ = ['valor', 'inicializar', 'consultas']
Importación absoluta vs. relativa
Importación absoluta: utiliza la ruta completa desde el paquete raíz.
# dentro de plataforma/api/versiones.py
from plataforma.comandos import gestion
gestion.ejecutar()
Importación relativa: utiliza puntos como referencia al directorio actual.
# dentro de plataforma/api/versiones.py
from ..comandos import gestion
gestion.ejecutar()
.representa el directorio actual..representa el directorio padre- Solo funciona dentro de un paquete, no entre directorios independientes
Las importaciones relativas simplifican la escritura pero solo pueden usarse entre módulos que pertenecen al mismo paquete. Las importaciones absolutas funcionan desde cualquier punto pero requieren especificar la ruta completa.
Estructura de un proyecto profesional
Un proyecto bien organizado separa las responsabilidades en distintos directorios y módulos:
proyecto/
├── inicio.py
├── configuracion/
│ ├── __init__.py
│ └── ajustes.py
├── nucleo/
│ ├── __init__.py
│ └── logica.py
├── utilidades/
│ ├── __init__.py
│ └── herramientas.py
├── almacenamiento/
│ └── datos.json
└── registros/
└── actividad.log
inicio.py - Punto de entrada del programa:
import sys, os
RUTA_BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(RUTA_BASE)
from nucleo import logica
if __name__ == '__main__':
logica.ejecutar()
configuracion/ajustes.py - Configuración centralizada:
import os
RUTA_BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
RUTA_BD = os.path.join(RUTA_BASE, 'almacenamiento', 'datos.json')
RUTA_LOG = os.path.join(RUTA_BASE, 'registros', 'actividad.log')
TIEMPO_SESION = 300
FORMATO_ESTANDAR = '[%(asctime)s][%(threadName)s:%(thread)d][%(name)s]' \
'[%(filename)s:%(lineno)d][%(levelname)s][%(message)s]'
FORMATO_SIMPLE = '[%(levelname)s][%(asctime)s][%(filename)s:%(lineno)d]%(message)s'
CONFIGURACION_LOG = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'estandar': {'format': FORMATO_ESTANDAR},
'simple': {'format': FORMATO_SIMPLE},
},
'handlers': {
'consola': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
'archivo': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'formatter': 'estandar',
'filename': RUTA_LOG,
'maxBytes': 5 * 1024 * 1024,
'backupCount': 5,
'encoding': 'utf-8',
},
},
'loggers': {
'': {
'handlers': ['archivo', 'consola'],
'level': 'DEBUG',
'propagate': True,
},
},
}
utilidades/herramientas.py - Funciones auxiliares:
from configuracion import ajustes
import logging
import logging.config
import json
def obtener_logger(nombre):
logging.config.dictConfig(ajustes.CONFIGURACION_LOG)
logger = logging.getLogger(nombre)
return logger
def cargar_base_datos():
ruta = ajustes.RUTA_BD
with open(ruta, 'r', encoding='utf-8') as archivo:
contenido = json.load(archivo)
return contenido
nucleo/logica.py - Lógica principal de la aplicación:
from configuracion import ajustes
from utilidades import herramientas
import time
registrador = herramientas.obtener_logger(__name__)
sesion_actual = {
'usuario': None,
'momento_acceso': None,
'expiracion': int(ajustes.TIEMPO_SESION)
}
def verificar_autenticacion(funcion):
def envoltura(*args, **kwargs):
if sesion_actual['usuario']:
transcurrido = time.time() - sesion_actual['momento_acceso']
if transcurrido < sesion_actual['expiracion']:
return funcion(*args, **kwargs)
usuario = input('Usuario: ')
clave = input('Contraseña: ')
base = herramientas.cargar_base_datos()
if usuario in base:
if clave == base[usuario].get('clave'):
registrador.info('Autenticación exitosa')
sesion_actual['usuario'] = usuario
sesion_actual['momento_acceso'] = time.time()
return funcion(*args, **kwargs)
else:
registrador.error('Usuario no encontrado')
return envoltura
@verificar_autenticacion
def comprar():
print('Procesando compra...')
def ejecutar():
print('''
1 - Comprar
2 - Ver saldo
3 - Transferir
''')
while True:
opcion = input('Seleccione: ').strip()
if not opcion:
continue
if opcion == '1':
comprar()
Este patrón de organización permite que el proyecto sea mantenible, escalable y fácil de entender para otros desarrolladores. Cada componente tiene una responsabilidad clara y las dependencias entre módulos se manejan de forma explícita.