Python

Python

Tutorial Python: Crear módulos y paquetes

Aprende a crear módulos y paquetes en Python con buenas prácticas para organizar, documentar y reutilizar código eficientemente.

Aprende Python y certifícate

Estructura de un módulo

Un módulo en Python es simplemente un archivo con extensión .py que contiene código Python reutilizable. Los módulos son la unidad básica de organización del código en Python, permitiéndonos encapsular funcionalidad relacionada y hacerla disponible para ser importada en otros scripts o módulos.

La estructura de un módulo bien diseñado sigue ciertas convenciones que facilitan su mantenimiento y uso. Veamos los elementos principales que componen un módulo Python:

Encabezado del módulo

Todo módulo debería comenzar con un docstring que describa su propósito, funcionalidad y uso. Esta documentación aparecerá cuando alguien utilice la función help() sobre el módulo:

"""
calculadora.py - Módulo para operaciones matemáticas básicas

Este módulo proporciona funciones para realizar operaciones aritméticas
simples como suma, resta, multiplicación y división.

Ejemplo de uso:
    >>> from calculadora import sumar
    >>> sumar(5, 3)
    8
"""

Importaciones

Después del docstring, se colocan las importaciones necesarias para el módulo. Es una buena práctica organizarlas en el siguiente orden:

  • Módulos de la biblioteca estándar
  • Módulos de terceros
  • Módulos locales de la aplicación
# Biblioteca estándar
import math
from datetime import datetime

# Bibliotecas de terceros
import numpy as np

# Módulos locales
from .utilidades import formatear_numero

Constantes y variables globales

Las constantes y variables globales del módulo se definen después de las importaciones. Por convención, las constantes se nombran en mayúsculas:

PI = 3.14159
GRAVEDAD = 9.8
VERSION = "1.0.0"

# Variables de configuración
nivel_precision = 2
modo_debug = False

Definición de clases y funciones

El cuerpo principal del módulo contiene las definiciones de clases, funciones y otras estructuras:

def sumar(a, b):
    """Suma dos números y devuelve el resultado."""
    return a + b

def restar(a, b):
    """Resta b de a y devuelve el resultado."""
    return a - b

class Calculadora:
    """Clase que implementa una calculadora básica."""
    
    def __init__(self, precision=2):
        """Inicializa la calculadora con un nivel de precisión."""
        self.precision = precision
    
    def dividir(self, a, b):
        """Divide a entre b con la precisión configurada."""
        if b == 0:
            raise ValueError("No se puede dividir por cero")
        resultado = a / b
        return round(resultado, self.precision)

Código de inicialización

Algunos módulos pueden contener código de inicialización que se ejecuta cuando el módulo se importa por primera vez:

# Inicialización del módulo
_cache = {}
_contador_llamadas = 0

print(f"Módulo calculadora versión {VERSION} cargado")

Bloque de ejecución principal

Un patrón común es incluir un bloque condicional al final del módulo que se ejecuta solo cuando el archivo se ejecuta directamente (no cuando se importa):

if __name__ == "__main__":
    # Este código solo se ejecuta cuando el módulo se ejecuta directamente
    print("Ejecutando pruebas del módulo calculadora...")
    
    # Pruebas simples
    assert sumar(5, 3) == 8
    assert restar(10, 4) == 6
    
    # Ejemplo de uso
    calc = Calculadora(precision=4)
    resultado = calc.dividir(10, 3)
    print(f"10 / 3 = {resultado}")

Variables privadas y públicas

En Python, por convención, los nombres que comienzan con un guion bajo (_) se consideran privados o de uso interno del módulo:

_contador_interno = 0  # Variable "privada"

def incrementar_contador():
    """Incrementa y devuelve el contador interno."""
    global _contador_interno
    _contador_interno += 1
    return _contador_interno

def obtener_contador():
    """Devuelve el valor actual del contador."""
    return _contador_interno

Control de exportaciones

Python permite controlar qué nombres se exportan cuando alguien usa from modulo import * mediante la variable especial __all__:

# Definir explícitamente qué se exporta con "from calculadora import *"
__all__ = ['sumar', 'restar', 'Calculadora', 'PI', 'VERSION']

Metadatos del módulo

Es común incluir metadatos como variables especiales que proporcionan información sobre el módulo:

__version__ = "1.0.0"
__author__ = "Ana García"
__email__ = "ana.garcia@ejemplo.com"
__license__ = "MIT"

Ejemplo completo de un módulo

Veamos un ejemplo completo que integra todos estos elementos:

"""
geometria.py - Módulo para cálculos geométricos básicos

Este módulo proporciona funciones y clases para realizar
cálculos geométricos como áreas y perímetros de figuras.

Ejemplo de uso:
    >>> from geometria import calcular_area_circulo
    >>> calcular_area_circulo(5)
    78.54
"""

# Importaciones
import math
from typing import Union, Tuple, List

# Constantes
PI = math.pi
PRECISION_DEFAULT = 2

# Variables de configuración
_usar_radianes = True
_cache_resultados = {}

# Definición de funciones
def calcular_area_circulo(radio: float, precision: int = PRECISION_DEFAULT) -> float:
    """
    Calcula el área de un círculo dado su radio.
    
    Args:
        radio: El radio del círculo
        precision: Número de decimales para redondear el resultado
        
    Returns:
        El área del círculo redondeada a la precisión especificada
    """
    clave_cache = (radio, precision)
    
    # Verificar si el resultado está en caché
    if clave_cache in _cache_resultados:
        return _cache_resultados[clave_cache]
    
    # Calcular y almacenar en caché
    area = PI * radio ** 2
    resultado = round(area, precision)
    _cache_resultados[clave_cache] = resultado
    
    return resultado

def calcular_perimetro_circulo(radio: float, precision: int = PRECISION_DEFAULT) -> float:
    """Calcula el perímetro de un círculo dado su radio."""
    perimetro = 2 * PI * radio
    return round(perimetro, precision)

# Definición de clases
class Rectangulo:
    """Clase que representa un rectángulo."""
    
    def __init__(self, ancho: float, alto: float):
        """Inicializa un rectángulo con ancho y alto dados."""
        self.ancho = ancho
        self.alto = alto
    
    def area(self) -> float:
        """Calcula y devuelve el área del rectángulo."""
        return self.ancho * self.alto
    
    def perimetro(self) -> float:
        """Calcula y devuelve el perímetro del rectángulo."""
        return 2 * (self.ancho + self.alto)

# Control de exportaciones
__all__ = [
    'calcular_area_circulo', 
    'calcular_perimetro_circulo', 
    'Rectangulo', 
    'PI'
]

# Metadatos
__version__ = "1.0.0"
__author__ = "Equipo de Desarrollo"

# Bloque de ejecución principal
if __name__ == "__main__":
    # Pruebas y ejemplos
    print(f"Área de un círculo de radio 5: {calcular_area_circulo(5)}")
    
    rect = Rectangulo(4, 5)
    print(f"Área del rectángulo: {rect.area()}")
    print(f"Perímetro del rectángulo: {rect.perimetro()}")

Buenas prácticas para estructurar módulos

  • Cohesión: Un módulo debe tener un propósito claro y contener código relacionado.
  • Tamaño adecuado: Si un módulo crece demasiado, considera dividirlo en varios módulos más pequeños.
  • Documentación: Documenta cada función, clase y método con docstrings informativos.
  • Tipado: Usa anotaciones de tipo para mejorar la claridad y permitir verificación estática.
  • Pruebas: Incluye pruebas unitarias, ya sea en el bloque if __name__ == "__main__" o en un módulo separado.
  • Nombres descriptivos: Usa nombres claros y descriptivos para funciones, clases y variables.

Siguiendo estas convenciones, crearás módulos bien estructurados que serán fáciles de entender, mantener y reutilizar en diferentes partes de tu proyecto o incluso en proyectos diferentes.

Archivo __init__.py

El archivo __init__.py es un componente fundamental en la creación de paquetes en Python. Su presencia en un directorio indica a Python que debe tratar ese directorio como un paquete, permitiendo que su contenido sea importado por otros módulos.

Propósito y funcionalidad básica

En su forma más simple, un archivo __init__.py puede estar completamente vacío. Incluso así, cumple su función principal: marcar un directorio como paquete de Python. Sin embargo, este archivo ofrece mucho más potencial que simplemente servir como marcador.

# Un archivo __init__.py vacío es válido y suficiente para crear un paquete

Inicialización del paquete

Uno de los usos más comunes del archivo __init__.py es realizar tareas de inicialización cuando se importa el paquete por primera vez:

"""
Paquete utilidades - Herramientas para procesamiento de datos

Este paquete proporciona funciones y clases para facilitar
el procesamiento y análisis de datos en aplicaciones Python.
"""

# Configuración inicial del paquete
DEBUG = False
VERSION = "2.1.0"

# Inicialización de recursos
print(f"Inicializando paquete utilidades v{VERSION}")

# Código de inicialización
import logging

# Configurar logging para el paquete
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

logger = logging.getLogger(__name__)
logger.info("Paquete utilidades cargado correctamente")

Control de importaciones con __all__

El archivo __init__.py permite definir qué módulos y símbolos se exponen cuando se utiliza la sintaxis from paquete import * mediante la variable especial __all__:

# Definir explícitamente qué se exporta con "from utilidades import *"
__all__ = [
    'formateo', 
    'validacion', 
    'conversion',
    'VERSION'
]

# Importar submódulos para hacerlos disponibles directamente
from . import formateo
from . import validacion
from . import conversion

# También podemos importar símbolos específicos de submódulos
from .constantes import VERSION, AUTOR

Simplificación de la interfaz del paquete

Un uso avanzado del archivo __init__.py es simplificar la interfaz del paquete, exponiendo directamente clases y funciones de submódulos:

# Importar clases y funciones específicas de submódulos
from .db.conexion import ConexionDB
from .db.consultas import ejecutar_consulta, obtener_resultados
from .archivos.csv import leer_csv, escribir_csv
from .archivos.json import cargar_json, guardar_json

# Ahora se pueden usar directamente:
# from mi_paquete import ConexionDB, leer_csv
# en lugar de:
# from mi_paquete.db.conexion import ConexionDB
# from mi_paquete.archivos.csv import leer_csv

Esta técnica permite crear una API limpia para tu paquete, ocultando la estructura interna de directorios y módulos.

Lazy loading con importaciones condicionales

Para paquetes grandes, podemos implementar carga perezosa (lazy loading) para mejorar el rendimiento:

# Definimos una función para importar módulos bajo demanda
def _import_module(name):
    import importlib
    return importlib.import_module(f".{name}", __name__)

# Diccionario para almacenar módulos ya importados
_cached_modules = {}

class LazyLoader:
    def __init__(self, module_name):
        self.module_name = module_name
    
    def __getattr__(self, name):
        if self.module_name not in _cached_modules:
            _cached_modules[self.module_name] = _import_module(self.module_name)
        return getattr(_cached_modules[self.module_name], name)

# Exponemos submódulos como objetos de carga perezosa
analisis = LazyLoader("analisis")  # Solo se cargará cuando se use
visualizacion = LazyLoader("visualizacion")  # Módulo pesado que se carga bajo demanda

Detección de dependencias opcionales

El archivo __init__.py es ideal para verificar dependencias opcionales y adaptar la funcionalidad del paquete:

# Intentar importar dependencias opcionales
try:
    import pandas as pd
    HAS_PANDAS = True
except ImportError:
    HAS_PANDAS = False
    
try:
    import matplotlib.pyplot as plt
    HAS_MATPLOTLIB = True
except ImportError:
    HAS_MATPLOTLIB = False

# Función que se adapta según las dependencias disponibles
def procesar_datos(datos):
    if HAS_PANDAS:
        # Usar pandas para procesamiento avanzado
        return pd.DataFrame(datos).describe()
    else:
        # Implementación alternativa sin pandas
        return {
            "count": len(datos),
            "mean": sum(datos) / len(datos) if datos else 0,
            "min": min(datos) if datos else None,
            "max": max(datos) if datos else None
        }

Compatibilidad entre versiones de Python

El archivo __init__.py también puede usarse para garantizar la compatibilidad entre diferentes versiones de Python:

import sys

# Verificar versión de Python
if sys.version_info < (3, 8):
    raise ImportError(
        f"Este paquete requiere Python 3.8 o superior, pero se está ejecutando "
        f"Python {sys.version_info.major}.{sys.version_info.minor}"
    )

# Importaciones condicionales según la versión
if sys.version_info >= (3, 9):
    # Usar características nuevas de Python 3.9+
    from collections import Counter
else:
    # Retrocompatibilidad para versiones anteriores
    from collections import Counter as _Counter
    
    class Counter(_Counter):
        # Implementación personalizada para añadir funcionalidad faltante
        def total(self):
            return sum(self.values())

Configuración dinámica del paquete

Podemos usar el archivo __init__.py para configurar dinámicamente el comportamiento del paquete:

import os

# Leer configuración desde variables de entorno
DEBUG = os.environ.get('APP_DEBUG', 'False').lower() in ('true', '1', 't')
API_KEY = os.environ.get('API_KEY', '')
MAX_CONNECTIONS = int(os.environ.get('MAX_CONNECTIONS', '5'))

# Configurar el paquete según el entorno
if os.environ.get('ENVIRONMENT') == 'production':
    # Configuración para producción
    LOG_LEVEL = 'ERROR'
    CACHE_ENABLED = True
    TIMEOUT = 30
else:
    # Configuración para desarrollo
    LOG_LEVEL = 'DEBUG' if DEBUG else 'INFO'
    CACHE_ENABLED = False
    TIMEOUT = 120

Ejemplo práctico: estructura completa

Veamos un ejemplo completo de un archivo __init__.py para un paquete de análisis de datos:

"""
analitica - Paquete para análisis y visualización de datos

Este paquete proporciona herramientas para procesar, analizar
y visualizar datos de diversas fuentes.
"""

# Metadatos del paquete
__version__ = "1.2.0"
__author__ = "Equipo de Ciencia de Datos"
__email__ = "datos@ejemplo.com"

# Control de exportaciones
__all__ = [
    'cargar_datos',
    'limpiar_datos',
    'analizar',
    'visualizar',
    'exportar',
    'VERSION'
]

# Constantes públicas
VERSION = __version__
FORMATOS_SOPORTADOS = ['csv', 'json', 'xlsx', 'sqlite']

# Importación de submódulos principales
from . import carga
from . import limpieza
from . import analisis
from . import visualizacion
from . import exportacion

# Exposición de funciones principales para simplificar la API
from .carga import cargar_archivo as cargar_datos
from .limpieza import normalizar as limpiar_datos
from .analisis import estadisticas as analizar
from .visualizacion import generar_grafico as visualizar
from .exportacion import guardar as exportar

# Inicialización y configuración
import logging
import os

# Configurar logging
logging.basicConfig(
    level=getattr(logging, os.environ.get('LOG_LEVEL', 'INFO')),
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

# Verificar dependencias opcionales
try:
    import pandas as pd
    import numpy as np
    HAS_ANALYTICS_LIBS = True
except ImportError:
    HAS_ANALYTICS_LIBS = False
    logging.warning(
        "Las bibliotecas pandas/numpy no están instaladas. "
        "Algunas funcionalidades estarán limitadas."
    )

# Función de inicialización
def inicializar(modo='auto', cache=True, max_workers=None):
    """
    Inicializa el paquete con configuración personalizada.
    
    Args:
        modo: Modo de operación ('auto', 'rápido', 'preciso')
        cache: Si se debe habilitar el caché de resultados
        max_workers: Número máximo de workers para procesamiento paralelo
    """
    from .config import configurar
    return configurar(modo=modo, cache=cache, max_workers=max_workers)

# Código que se ejecuta al importar el paquete
logger = logging.getLogger(__name__)
logger.debug(f"Paquete analitica v{__version__} inicializado")

Buenas prácticas para el archivo __init__.py

  • Mantenerlo ligero: Evita código pesado que ralentice la importación del paquete.
  • Documentación clara: Incluye un docstring que describa el propósito del paquete.
  • API consistente: Expón una interfaz limpia y bien pensada.
  • Evitar efectos secundarios: Minimiza el código que produce efectos al importar.
  • Gestión de dependencias: Maneja elegantemente las dependencias opcionales.
  • Retrocompatibilidad: Asegura que el paquete funcione en diferentes entornos.

El archivo __init__.py es mucho más que un simple marcador de paquetes; es una poderosa herramienta para diseñar la interfaz de tu paquete y controlar cómo interactúan con él otros desarrolladores.

Organización de paquetes

La organización adecuada de paquetes en Python es fundamental para crear proyectos mantenibles y escalables. Un paquete bien estructurado facilita la navegación, mejora la colaboración entre desarrolladores y permite una mejor reutilización del código.

Estructura de directorios recomendada

La estructura de directorios de un paquete Python debe reflejar la organización lógica de su funcionalidad. Una estructura básica pero efectiva podría ser:

mi_paquete/
│
├── __init__.py
├── modulo1.py
├── modulo2.py
│
├── subpaquete1/
│   ├── __init__.py
│   ├── modulo_a.py
│   └── modulo_b.py
│
└── subpaquete2/
    ├── __init__.py
    └── modulo_c.py

Esta estructura permite importaciones claras y predecibles:

from mi_paquete import modulo1
from mi_paquete.subpaquete1 import modulo_a

Estructura para proyectos más complejos

Para proyectos de mayor envergadura, es recomendable seguir una estructura más elaborada que separe claramente las diferentes responsabilidades:

proyecto/
│
├── mi_paquete/                  # Código fuente principal
│   ├── __init__.py
│   ├── core/                    # Funcionalidad central
│   │   ├── __init__.py
│   │   └── ...
│   ├── utils/                   # Utilidades generales
│   │   ├── __init__.py
│   │   └── ...
│   └── api/                     # Interfaces públicas
│       ├── __init__.py
│       └── ...
│
├── tests/                       # Pruebas unitarias y de integración
│   ├── __init__.py
│   ├── test_core.py
│   └── ...
│
├── docs/                        # Documentación
│   ├── conf.py
│   └── index.rst
│
├── examples/                    # Ejemplos de uso
│   └── ejemplo_basico.py
│
├── pyproject.toml              # Configuración del proyecto (PEP 621)
├── README.md                   # Documentación principal
└── LICENSE                     # Licencia del proyecto

Principios de organización

Al estructurar tus paquetes, considera estos principios fundamentales:

  • Cohesión: Cada módulo debe tener un propósito claro y bien definido.
  • Separación de responsabilidades: Divide tu código según su función (lógica de negocio, utilidades, interfaces, etc.).
  • Profundidad adecuada: Evita jerarquías demasiado profundas que compliquen las importaciones.
  • Consistencia: Mantén un estilo coherente en la organización de todos los módulos.

Patrones comunes de organización

Existen varios patrones establecidos para organizar paquetes Python según su propósito:

Patrón por funcionalidad

Organiza los módulos según su función en el sistema:

mi_app/
├── __init__.py
├── modelos/
│   ├── __init__.py
│   ├── usuario.py
│   └── producto.py
├── vistas/
│   ├── __init__.py
│   ├── admin.py
│   └── cliente.py
└── controladores/
    ├── __init__.py
    ├── autenticacion.py
    └── pedidos.py

Patrón por dominio

Organiza los módulos por dominio de negocio, agrupando toda la funcionalidad relacionada:

mi_app/
├── __init__.py
├── usuarios/
│   ├── __init__.py
│   ├── modelos.py
│   ├── vistas.py
│   └── controladores.py
└── productos/
    ├── __init__.py
    ├── modelos.py
    ├── vistas.py
    └── controladores.py

Gestión de dependencias internas

Un aspecto importante de la organización de paquetes es cómo manejar las dependencias entre módulos. Existen varias estrategias:

Importaciones absolutas

Las importaciones absolutas son claras y no ambiguas:

# En mi_paquete/subpaquete1/modulo_a.py
from mi_paquete.modulo1 import funcion_util
from mi_paquete.subpaquete2 import modulo_c

Importaciones relativas

Las importaciones relativas son útiles para referencias dentro del mismo paquete:

# En mi_paquete/subpaquete1/modulo_a.py
from .. import modulo1  # Sube un nivel y accede a modulo1
from ..subpaquete2 import modulo_c  # Sube un nivel y accede a subpaquete2/modulo_c
from . import modulo_b  # Accede a un módulo en el mismo directorio

Organización para distribución

Si planeas distribuir tu paquete a través de PyPI, necesitas una estructura que soporte la instalación adecuada:

mi_proyecto/
│
├── src/                         # Código fuente en subdirectorio
│   └── mi_paquete/
│       ├── __init__.py
│       └── ...
│
├── tests/                       # Pruebas fuera del paquete
│   └── ...
│
├── pyproject.toml               # Configuración moderna (PEP 621)
├── setup.py                     # Script de instalación (opcional/legado)
├── setup.cfg                    # Configuración adicional (opcional)
└── README.md                    # Documentación

Esta estructura con el código fuente en un subdirectorio src/ es cada vez más recomendada porque:

  • Evita importaciones accidentales del código sin instalar
  • Garantiza que las pruebas se ejecuten contra el paquete instalado
  • Previene problemas comunes de importación durante el desarrollo

Ejemplo práctico: biblioteca de análisis de datos

Veamos un ejemplo concreto de organización para una biblioteca de análisis de datos:

datanalysis/
│
├── src/
│   └── datanalysis/
│       ├── __init__.py
│       ├── io/                  # Entrada/salida de datos
│       │   ├── __init__.py
│       │   ├── csv_reader.py
│       │   ├── json_reader.py
│       │   └── database.py
│       │
│       ├── processing/          # Procesamiento de datos
│       │   ├── __init__.py
│       │   ├── cleaning.py
│       │   ├── transformation.py
│       │   └── aggregation.py
│       │
│       ├── analysis/            # Análisis estadístico
│       │   ├── __init__.py
│       │   ├── descriptive.py
│       │   ├── inferential.py
│       │   └── timeseries.py
│       │
│       ├── visualization/       # Visualización
│       │   ├── __init__.py
│       │   ├── plots.py
│       │   └── dashboards.py
│       │
│       └── utils/               # Utilidades generales
│           ├── __init__.py
│           ├── validation.py
│           └── logging.py
│
├── tests/
│   ├── test_io.py
│   ├── test_processing.py
│   └── ...
│
├── examples/
│   ├── basic_analysis.py
│   └── advanced_visualization.py
│
├── pyproject.toml
└── README.md

Implementación de los archivos __init__.py

La forma en que configures tus archivos __init__.py afecta directamente a cómo se utilizará tu paquete. Veamos algunos ejemplos para la estructura anterior:

Archivo __init__.py principal

"""
datanalysis - Biblioteca para análisis de datos en Python

Proporciona herramientas para cargar, procesar, analizar y visualizar datos.
"""

__version__ = "0.1.0"

# Exponer las funciones más utilizadas para facilitar su importación
from .io.csv_reader import read_csv
from .io.json_reader import read_json
from .processing.cleaning import clean_dataset
from .analysis.descriptive import describe
from .visualization.plots import plot_histogram, plot_scatter

# Definir qué se importa con from datanalysis import *
__all__ = [
    'read_csv', 'read_json', 'clean_dataset', 
    'describe', 'plot_histogram', 'plot_scatter'
]

Archivo __init__.py de un subpaquete

"""
Módulo de visualización para datanalysis.

Proporciona funciones para crear visualizaciones estáticas e interactivas.
"""

# Importar funciones principales para hacerlas disponibles directamente
from .plots import (
    plot_histogram, plot_scatter, plot_line, 
    plot_bar, plot_heatmap
)

# Definir qué se importa con from datanalysis.visualization import *
__all__ = [
    'plot_histogram', 'plot_scatter', 'plot_line', 
    'plot_bar', 'plot_heatmap', 'dashboards'
]

# Importar el módulo completo para acceso mediante datanalysis.visualization.dashboards
from . import dashboards

Uso de namespaces para evitar conflictos

Al organizar paquetes grandes, es importante evitar conflictos de nombres. Una estrategia es usar namespaces claros y consistentes:

# En lugar de:
from datanalysis.io import read_csv
from datanalysis.processing import clean

# Puedes usar namespaces más explícitos:
import datanalysis.io as data_io
import datanalysis.processing as data_proc

# Uso:
df = data_io.read_csv("datos.csv")
clean_df = data_proc.clean(df)

Herramientas para gestionar la estructura de paquetes

Python ofrece varias herramientas que facilitan la creación y mantenimiento de paquetes bien organizados:

  • cookiecutter: Genera estructuras de proyectos a partir de plantillas
  • poetry: Gestiona dependencias y la estructura del proyecto
  • setuptools: Configura paquetes para distribución
  • isort: Ordena automáticamente las importaciones
  • black: Formatea el código siguiendo un estilo consistente

Ejemplo de uso de cookiecutter para crear un nuevo paquete:

pip install cookiecutter
cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage

Consideraciones para proyectos colaborativos

En proyectos con múltiples colaboradores, es especialmente importante:

  • Documentar claramente la estructura del paquete
  • Establecer convenciones de importación
  • Definir responsabilidades claras para cada módulo
  • Mantener un archivo README actualizado con la estructura
  • Usar herramientas de formateo automático para mantener consistencia

Ejemplo de documentación de estructura

Es útil mantener un documento que explique la estructura del paquete:

# Estructura del paquete datanalysis

## Módulos principales

- `io/`: Funciones para leer y escribir datos desde diferentes fuentes
  - `csv_reader.py`: Lectura y escritura de archivos CSV
  - `json_reader.py`: Procesamiento de datos JSON
  - `database.py`: Conexiones a bases de datos

- `processing/`: Herramientas para procesar y transformar datos
  - `cleaning.py`: Limpieza de datos (valores nulos, duplicados)
  - `transformation.py`: Transformaciones (normalización, codificación)
  - `aggregation.py`: Funciones de agregación y resumen

...

## Convenciones de importación

- Usar importaciones absolutas para referencias entre subpaquetes
- Preferir `import módulo` sobre `from módulo import *`
- Para funciones muy utilizadas, exponerlas en el __init__.py del paquete

La organización efectiva de paquetes es un arte que evoluciona con la experiencia. Comenzar con una estructura clara y bien pensada ahorrará tiempo y esfuerzo a medida que tu proyecto crezca, facilitando su mantenimiento y extensión a largo plazo.

Namespaces y subpaquetes

Los namespaces en Python son una característica fundamental que permite organizar y estructurar el código de manera jerárquica, evitando conflictos de nombres entre diferentes partes de un programa. Cuando trabajamos con paquetes complejos, entender cómo funcionan los namespaces y cómo organizar subpaquetes se vuelve esencial para crear código mantenible y escalable.

Entendiendo los namespaces en Python

Un namespace es básicamente un mapeo entre nombres y objetos. En Python, todo es un objeto, y los namespaces proporcionan una forma de acceder a estos objetos mediante nombres únicos dentro de un contexto específico. Cuando importamos un módulo o paquete, estamos accediendo a su namespace.

# Cada módulo tiene su propio namespace
import math
import statistics

# Accedemos a funciones en diferentes namespaces
radio = 5
area = math.pi * radio**2
media = statistics.mean([1, 2, 3, 4, 5])

En este ejemplo, math y statistics son namespaces separados que contienen sus propias funciones y constantes, evitando conflictos aunque ambos módulos pudieran tener funciones con nombres idénticos.

Subpaquetes: extendiendo la organización

Los subpaquetes son paquetes anidados dentro de otros paquetes, creando una estructura jerárquica que permite organizar código relacionado en categorías y subcategorías lógicas. Esta estructura facilita la navegación y comprensión del código, especialmente en proyectos grandes.

Para crear un subpaquete, simplemente creamos un directorio dentro de otro paquete e incluimos un archivo __init__.py:

mi_aplicacion/
├── __init__.py
├── principal.py
└── utilidades/
    ├── __init__.py
    ├── formateo.py
    └── validacion.py

Importación de subpaquetes

Existen varias formas de importar y utilizar subpaquetes:

  • Importación completa del subpaquete:
import mi_aplicacion.utilidades

# Uso
resultado = mi_aplicacion.utilidades.formateo.formatear_texto("ejemplo")
  • Importación directa de un módulo del subpaquete:
from mi_aplicacion.utilidades import formateo

# Uso más conciso
resultado = formateo.formatear_texto("ejemplo")
  • Importación de funciones específicas:
from mi_aplicacion.utilidades.formateo import formatear_texto

# Uso directo de la función
resultado = formatear_texto("ejemplo")

Gestión de namespaces en subpaquetes

El archivo __init__.py de cada subpaquete juega un papel crucial en la gestión de namespaces. Podemos usarlo para controlar qué se expone cuando se importa el subpaquete:

# mi_aplicacion/utilidades/__init__.py

# Importar y exponer funciones específicas
from .formateo import formatear_texto, formatear_numero
from .validacion import validar_email, validar_telefono

# Definir explícitamente qué se exporta
__all__ = [
    'formatear_texto', 
    'formatear_numero', 
    'validar_email', 
    'validar_telefono'
]

Con esta configuración, cuando alguien use from mi_aplicacion.utilidades import *, solo obtendrá las funciones especificadas en __all__.

Importaciones relativas en subpaquetes

Las importaciones relativas son especialmente útiles dentro de subpaquetes para referenciar otros módulos del mismo paquete o subpaquete:

# En mi_aplicacion/utilidades/formateo.py

# Importación relativa de un módulo en el mismo subpaquete
from . import validacion

# Importación relativa de un módulo en el paquete padre
from .. import principal

# Importación relativa de un módulo en otro subpaquete (si existiera)
from ..otro_subpaquete import algun_modulo

Las importaciones relativas hacen que el código sea más mantenible, ya que no dependen de la ubicación absoluta del paquete en el sistema de archivos.

Namespaces anidados y resolución de nombres

Cuando trabajamos con subpaquetes, Python utiliza un sistema de resolución de nombres jerárquico:

import mi_aplicacion.utilidades.formateo

# Python busca:
# 1. El módulo 'mi_aplicacion'
# 2. El subpaquete 'utilidades' dentro de 'mi_aplicacion'
# 3. El módulo 'formateo' dentro del subpaquete 'utilidades'

Esta jerarquía de namespaces permite una organización clara y evita conflictos de nombres entre diferentes partes de la aplicación.

Subpaquetes implícitos (Python 3.3+)

A partir de Python 3.3, se introdujo el concepto de subpaquetes implícitos, que permite que un directorio sea tratado como un subpaquete incluso sin un archivo __init__.py:

mi_aplicacion/
├── __init__.py
└── datos/
    └── config.py  # Accesible como mi_aplicacion.datos.config

Sin embargo, para mantener la compatibilidad y aprovechar las funcionalidades que ofrece __init__.py, se recomienda seguir incluyendo este archivo en todos los subpaquetes.

Namespaces y colisiones de nombres

Un beneficio clave de los namespaces es evitar colisiones de nombres. Consideremos este ejemplo:

# Tenemos dos módulos con funciones de nombre idéntico
from mi_aplicacion.procesamiento import analizar
from mi_aplicacion.reportes import analizar

# Esto causaría un problema - la segunda importación sobrescribe la primera
resultado = analizar(datos)  # ¿Qué función se ejecuta?

La solución es usar namespaces explícitos:

import mi_aplicacion.procesamiento as proc
import mi_aplicacion.reportes as rep

# Ahora es claro qué función estamos usando
resultado_proc = proc.analizar(datos)
resultado_rep = rep.analizar(datos)

Organización de subpaquetes por funcionalidad

Una estrategia común es organizar subpaquetes según su funcionalidad. Por ejemplo, para una aplicación de análisis de datos:

analisis_datos/
├── __init__.py
├── io/                  # Entrada/salida
│   ├── __init__.py
│   ├── lectores.py
│   └── escritores.py
├── procesamiento/       # Procesamiento de datos
│   ├── __init__.py
│   ├── limpieza.py
│   └── transformacion.py
└── visualizacion/       # Visualización
    ├── __init__.py
    ├── graficos.py
    └── reportes.py

Esta estructura facilita encontrar código relacionado y entender la arquitectura general del sistema.

Ejemplo práctico: biblioteca de análisis de texto

Veamos un ejemplo práctico de cómo organizar una biblioteca de análisis de texto con subpaquetes:

textanalysis/
├── __init__.py
├── preprocesamiento/
│   ├── __init__.py
│   ├── tokenizacion.py
│   ├── normalizacion.py
│   └── filtrado.py
├── analisis/
│   ├── __init__.py
│   ├── frecuencias.py
│   ├── sentimientos.py
│   └── entidades.py
└── utilidades/
    ├── __init__.py
    ├── archivos.py
    └── visualizacion.py

Implementación del archivo principal __init__.py:

"""
textanalysis - Biblioteca para análisis de texto en Python

Proporciona herramientas para preprocesar, analizar y visualizar datos textuales.
"""

__version__ = "0.1.0"

# Importar subpaquetes principales para hacerlos accesibles directamente
from . import preprocesamiento
from . import analisis
from . import utilidades

# Exponer funciones comunes para facilitar su uso
from .preprocesamiento.tokenizacion import tokenizar_texto
from .preprocesamiento.normalizacion import normalizar_texto
from .analisis.frecuencias import calcular_frecuencias
from .analisis.sentimientos import analizar_sentimiento

# Definir alias para namespaces largos
preproc = preprocesamiento
util = utilidades

# Definir qué se importa con from textanalysis import *
__all__ = [
    'tokenizar_texto',
    'normalizar_texto',
    'calcular_frecuencias',
    'analizar_sentimiento',
    'preprocesamiento',
    'analisis',
    'utilidades'
]

Implementación de un archivo __init__.py de subpaquete:

"""
Subpaquete de preprocesamiento para textanalysis.

Contiene funciones para tokenizar, normalizar y filtrar texto.
"""

# Importar funciones principales para hacerlas disponibles directamente
from .tokenizacion import tokenizar_texto, tokenizar_oraciones
from .normalizacion import normalizar_texto, eliminar_acentos
from .filtrado import eliminar_stopwords, filtrar_por_longitud

# Definir qué se importa con from textanalysis.preprocesamiento import *
__all__ = [
    'tokenizar_texto',
    'tokenizar_oraciones',
    'normalizar_texto',
    'eliminar_acentos',
    'eliminar_stopwords',
    'filtrar_por_longitud'
]

Uso de la biblioteca con namespaces

Con esta estructura, podemos usar la biblioteca de diferentes formas:

# Importación completa con namespaces explícitos
import textanalysis as ta

tokens = ta.preprocesamiento.tokenizacion.tokenizar_texto("Ejemplo de texto")
sentimiento = ta.analisis.sentimientos.analizar_sentimiento(tokens)

# Usando los alias definidos
tokens = ta.preproc.tokenizacion.tokenizar_texto("Ejemplo de texto")

# Importación directa de subpaquetes
from textanalysis import preprocesamiento, analisis

tokens = preprocesamiento.tokenizacion.tokenizar_texto("Ejemplo de texto")
sentimiento = analisis.sentimientos.analizar_sentimiento(tokens)

# Importación de funciones específicas
from textanalysis import tokenizar_texto, analizar_sentimiento

tokens = tokenizar_texto("Ejemplo de texto")
sentimiento = analizar_sentimiento(tokens)

Patrones avanzados para subpaquetes

Lazy loading en subpaquetes

Para paquetes grandes con muchos subpaquetes, podemos implementar carga perezosa para mejorar el rendimiento:

# En textanalysis/__init__.py

class LazySubpackageLoader:
    def __init__(self, name):
        self.name = name
        self._module = None
    
    def __getattr__(self, attr):
        if self._module is None:
            import importlib
            self._module = importlib.import_module(f".{self.name}", __package__)
        return getattr(self._module, attr)

# Cargar subpaquetes solo cuando se accede a ellos
avanzado = LazySubpackageLoader("avanzado")  # Subpaquete pesado

Subpaquetes condicionales

Podemos adaptar la disponibilidad de subpaquetes según las dependencias instaladas:

# En textanalysis/__init__.py

try:
    from . import visualizacion_avanzada
    HAS_VISUALIZATION = True
except ImportError:
    HAS_VISUALIZATION = False
    
def mostrar_grafico(datos, tipo="barras"):
    if not HAS_VISUALIZATION and tipo != "barras":
        raise ImportError("Se requiere instalar dependencias adicionales para este tipo de gráfico")
    
    if tipo == "barras":
        from .utilidades.visualizacion import grafico_barras
        return grafico_barras(datos)
    else:
        return visualizacion_avanzada.generar_grafico(datos, tipo)

Buenas prácticas para namespaces y subpaquetes

  • Nombres descriptivos: Usa nombres claros para paquetes y subpaquetes que indiquen su propósito.
  • Profundidad adecuada: Evita estructuras demasiado profundas (más de 3-4 niveles) que compliquen las importaciones.
  • Consistencia: Mantén un estilo coherente en todos los subpaquetes.
  • Documentación: Documenta la estructura y propósito de cada subpaquete en su archivo __init__.py.
  • Importaciones explícitas: Prefiere importaciones explícitas sobre import * para mayor claridad.
  • API limpia: Usa los archivos __init__.py para exponer una API limpia y ocultar detalles de implementación.

Los namespaces y subpaquetes son herramientas poderosas para organizar código Python a gran escala. Cuando se utilizan correctamente, permiten crear bibliotecas y aplicaciones bien estructuradas, mantenibles y fáciles de entender, incluso a medida que crecen en complejidad y tamaño.

Aprende Python online

Otras lecciones de Python

Accede a todas las lecciones de Python y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A Python

Python

Introducción

Instalación Y Creación De Proyecto

Python

Introducción

Tema 2: Tipos De Datos, Variables Y Operadores

Python

Introducción

Instalación De Python

Python

Introducción

Tipos De Datos

Python

Sintaxis

Variables

Python

Sintaxis

Operadores

Python

Sintaxis

Estructuras De Control

Python

Sintaxis

Funciones

Python

Sintaxis

Estructuras Control Iterativo

Python

Sintaxis

Estructuras Control Condicional

Python

Sintaxis

Testing Con Pytest

Python

Sintaxis

Listas

Python

Estructuras De Datos

Tuplas

Python

Estructuras De Datos

Diccionarios

Python

Estructuras De Datos

Conjuntos

Python

Estructuras De Datos

Comprehensions

Python

Estructuras De Datos

Clases Y Objetos

Python

Programación Orientada A Objetos

Excepciones

Python

Programación Orientada A Objetos

Encapsulación

Python

Programación Orientada A Objetos

Herencia

Python

Programación Orientada A Objetos

Polimorfismo

Python

Programación Orientada A Objetos

Mixins Y Herencia Múltiple

Python

Programación Orientada A Objetos

Métodos Especiales (Dunder Methods)

Python

Programación Orientada A Objetos

Composición De Clases

Python

Programación Orientada A Objetos

Funciones Lambda

Python

Programación Funcional

Aplicación Parcial

Python

Programación Funcional

Entrada Y Salida, Manejo De Archivos

Python

Programación Funcional

Decoradores

Python

Programación Funcional

Generadores

Python

Programación Funcional

Paradigma Funcional

Python

Programación Funcional

Composición De Funciones

Python

Programación Funcional

Funciones Orden Superior Map Y Filter

Python

Programación Funcional

Funciones Auxiliares

Python

Programación Funcional

Reducción Y Acumulación

Python

Programación Funcional

Archivos Comprimidos

Python

Entrada Y Salida Io

Entrada Y Salida Avanzada

Python

Entrada Y Salida Io

Archivos Temporales

Python

Entrada Y Salida Io

Contexto With

Python

Entrada Y Salida Io

Módulo Csv

Python

Biblioteca Estándar

Módulo Json

Python

Biblioteca Estándar

Módulo Datetime

Python

Biblioteca Estándar

Módulo Math

Python

Biblioteca Estándar

Módulo Os

Python

Biblioteca Estándar

Módulo Re

Python

Biblioteca Estándar

Módulo Random

Python

Biblioteca Estándar

Módulo Time

Python

Biblioteca Estándar

Módulo Collections

Python

Biblioteca Estándar

Módulo Sys

Python

Biblioteca Estándar

Módulo Statistics

Python

Biblioteca Estándar

Módulo Pickle

Python

Biblioteca Estándar

Módulo Pathlib

Python

Biblioteca Estándar

Importar Módulos Y Paquetes

Python

Paquetes Y Módulos

Crear Módulos Y Paquetes

Python

Paquetes Y Módulos

Entornos Virtuales (Virtualenv, Venv)

Python

Entorno Y Dependencias

Gestión De Dependencias (Pip, Requirements.txt)

Python

Entorno Y Dependencias

Python-dotenv Y Variables De Entorno

Python

Entorno Y Dependencias

Acceso A Datos Con Mysql, Pymongo Y Pandas

Python

Acceso A Bases De Datos

Acceso A Mongodb Con Pymongo

Python

Acceso A Bases De Datos

Acceso A Mysql Con Mysql Connector

Python

Acceso A Bases De Datos

Novedades Python 3.13

Python

Características Modernas

Operador Walrus

Python

Características Modernas

Pattern Matching

Python

Características Modernas

Instalación Beautiful Soup

Python

Web Scraping

Sintaxis General De Beautiful Soup

Python

Web Scraping

Tipos De Selectores

Python

Web Scraping

Web Scraping De Html

Python

Web Scraping

Web Scraping Para Ciencia De Datos

Python

Web Scraping

Autenticación Y Acceso A Recursos Protegidos

Python

Web Scraping

Combinación De Selenium Con Beautiful Soup

Python

Web Scraping

Accede GRATIS a Python y certifícate

Ejercicios de programación de Python

Evalúa tus conocimientos de esta lección Crear módulos y paquetes con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Módulo math

Python
Puzzle

Reto herencia

Python
Código

Excepciones

Python
Test

Introducción a Python

Python
Test

Reto variables

Python
Código

Funciones Python

Python
Puzzle

Reto funciones

Python
Código

Módulo datetime

Python
Test

Reto acumulación

Python
Código

Reto estructuras condicionales

Python
Código

Polimorfismo

Python
Test

Módulo os

Python
Test

Reto métodos dunder

Python
Código

Diccionarios

Python
Puzzle

Reto clases y objetos

Python
Código

Reto operadores

Python
Código

Operadores

Python
Test

Estructuras de control

Python
Puzzle

Funciones lambda

Python
Test

Reto diccionarios

Python
Código

Reto función lambda

Python
Código

Encapsulación

Python
Puzzle

Reto coleciones

Python
Proyecto

Reto funciones auxiliares

Python
Código

Crear módulos y paquetes

Python
Puzzle

Módulo datetime

Python
Puzzle

Excepciones

Python
Puzzle

Operadores

Python
Puzzle

Diccionarios

Python
Test

Reto map, filter

Python
Código

Reto tuplas

Python
Código

Proyecto gestor de tareas CRUD

Python
Proyecto

Tuplas

Python
Puzzle

Variables

Python
Puzzle

Tipos de datos

Python
Puzzle

Conjuntos

Python
Test

Reto mixins

Python
Código

Módulo csv

Python
Test

Módulo json

Python
Test

Herencia

Python
Test

Análisis de datos de ventas con Pandas

Python
Proyecto

Reto fechas y tiempo

Python
Proyecto

Reto estructuras de iteración

Python
Código

Funciones

Python
Test

Reto comprehensions

Python
Código

Variables

Python
Test

Reto serialización

Python
Proyecto

Módulo csv

Python
Puzzle

Reto polimorfismo

Python
Código

Polimorfismo

Python
Puzzle

Clases y objetos

Python
Código

Reto encapsulación

Python
Código

Estructuras de control

Python
Test

Importar módulos y paquetes

Python
Test

Módulo math

Python
Test

Funciones lambda

Python
Puzzle

Reto excepciones

Python
Código

Listas

Python
Puzzle

Reto archivos

Python
Proyecto

Encapsulación

Python
Test

Reto conjuntos

Python
Código

Clases y objetos

Python
Test

Instalación de Python y creación de proyecto

Python
Test

Reto listas

Python
Código

Tipos de datos

Python
Test

Crear módulos y paquetes

Python
Test

Tuplas

Python
Test

Herencia

Python
Puzzle

Reto acceso a sistema

Python
Proyecto

Proyecto sintaxis calculadora

Python
Proyecto

Importar módulos y paquetes

Python
Puzzle

Clases y objetos

Python
Puzzle

Módulo os

Python
Puzzle

Listas

Python
Test

Conjuntos

Python
Puzzle

Reto tipos de datos

Python
Código

Reto matemáticas

Python
Proyecto

Módulo json

Python
Puzzle

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender la estructura y componentes de un módulo Python.
  • Aprender a crear y configurar el archivo init.py para definir paquetes.
  • Conocer las buenas prácticas para organizar paquetes y subpaquetes en proyectos Python.
  • Entender el concepto de namespaces y cómo gestionar subpaquetes para evitar conflictos de nombres.
  • Aplicar patrones de importación y organización para mejorar la mantenibilidad y escalabilidad del código.