Python & AI Tutorials Logo
Programación Python

22. Organizar el código con módulos y paquetes

A medida que tus programas de Python crecen, mantener todo tu código en un solo archivo se vuelve poco práctico. Querrás organizar funciones, clases y variables relacionadas en archivos separados que puedan reutilizarse en diferentes programas. El sistema de módulos y paquetes de Python ofrece exactamente esta capacidad: una forma de organizar, compartir y reutilizar código de manera eficaz.

En este capítulo, exploraremos cómo funciona el sistema de importación de Python, cómo crear y usar tus propios módulos, y cómo organizar varios módulos en paquetes. También examinaremos la variable especial __name__, que te permite escribir archivos que funcionan tanto como módulos importables como scripts independientes.

22.1) Qué son los módulos y cómo funciona import

Comprender los módulos

Un módulo es simplemente un archivo de Python que contiene definiciones y sentencias. Cualquier archivo .py que crees es un módulo. Cuando escribes una función en un archivo llamado calculator.py, ese archivo se convierte en un módulo llamado calculator que otros archivos de Python pueden usar.

Los módulos cumplen varios propósitos importantes:

  • Reutilización de código: Escribe una función una vez y úsala en varios programas
  • Organización: Agrupa funcionalidades relacionadas
  • Gestión del espacio de nombres: Mantén los nombres separados para evitar conflictos
  • Mantenibilidad: Archivos más pequeños y enfocados son más fáciles de entender y modificar

Vamos a crear un módulo simple para ver cómo funciona. Crea un archivo llamado greetings.py:

python
# greetings.py
def say_hello(name):
    """Return a friendly greeting."""
    return f"Hello, {name}!"
 
def say_goodbye(name):
    """Return a farewell message."""
    return f"Goodbye, {name}. See you soon!"
 
# Una variable a nivel de módulo
default_greeting = "Welcome"

Este archivo ahora es un módulo. Contiene dos funciones y una variable que otros archivos de Python pueden usar.

La sentencia import

Para usar código de un módulo, lo importas. La sentencia import le dice a Python que cargue un módulo y ponga su contenido disponible. Crea otro archivo en el mismo directorio llamado main.py:

python
# main.py
import greetings
 
message = greetings.say_hello("Alice")
print(message)  # Output: Hello, Alice!
 
farewell = greetings.say_goodbye("Bob")
print(farewell)  # Output: Goodbye, Bob. See you soon!
 
print(greetings.default_greeting)  # Output: Welcome

Cuando ejecutas main.py, Python ejecuta la sentencia import greetings. Esto es lo que ocurre entre bambalinas:

No

import greetings

¿greetings
ya está importado?

Buscar greetings.py

Ejecutar greetings.py
de arriba a abajo

Crear un objeto de módulo
llamado 'greetings'

Almacenar funciones y variables
como atributos

Poner 'greetings' disponible
en el espacio de nombres actual

Usar el objeto de módulo
existente

Importante: Python ejecuta el código de un módulo solo la primera vez que se importa en un programa. Las importaciones posteriores en el mismo programa reutilizan el módulo ya cargado. Esto evita ejecuciones duplicadas y ahorra tiempo.

Acceder al contenido de un módulo

Después de importar un módulo, accedes a su contenido usando notación de punto(dot notation): module_name.item_name. Esto es similar a cómo accedemos a métodos de cadenas como text.upper() o métodos de listas como numbers.append(), como aprendimos en los Capítulos 5 y 14.

python
import greetings
 
# Acceder a funciones
result = greetings.say_hello("Charlie")
 
# Acceder a variables
greeting = greetings.default_greeting
 
# Incluso puedes comprobar qué hay en un módulo
print(dir(greetings))  # Muestra todos los nombres definidos en el módulo

La notación de punto deja claro de dónde viene cada nombre. Cuando ves greetings.say_hello(), sabes inmediatamente que esta función viene del módulo greetings.

Ruta de búsqueda de módulos

Cuando escribes import greetings, ¿cómo encuentra Python greetings.py? Python busca módulos en un orden específico:

  1. Directorio actual: El directorio que contiene el script que estás ejecutando
  2. PYTHONPATH: Directorios listados en la variable de entorno PYTHONPATH (si está configurada)
  3. Biblioteca estándar: Directorios de módulos integrados de Python
  4. Site-Packages: Paquetes de terceros instalados con pip

Puedes ver la ruta de búsqueda de Python examinando sys.path:

python
import sys
 
for path in sys.path:
    print(path)

Salida (ejemplo: tus rutas reales variarán según tu sistema y la instalación de Python):

/home/user/projects/myproject
/usr/lib/python3.11
/usr/lib/python3.11/lib-dynload
/usr/local/lib/python3.11/site-packages

La primera ruta en la salida es el directorio de trabajo actual. Python busca primero en este directorio, por lo que puede encontrar módulos en el mismo directorio.

Nombres de módulos y nombres de archivos

El nombre del módulo es el nombre del archivo sin la extensión .py. Si tu archivo es string_utils.py, el nombre del módulo es string_utils. Los nombres de módulos deben seguir las reglas de identificadores de Python (como aprendimos en el Capítulo 3):

  • Empezar con una letra o guion bajo
  • Contener solo letras, dígitos y guiones bajos
  • No pueden ser palabras clave de Python
python
# Nombres de módulos válidos (y nombres de archivo)
import data_processor      # data_processor.py
import user_auth          # user_auth.py
import _internal_helpers  # _internal_helpers.py
 
# Invalid - would cause errors
# import 2d_graphics       # Can't start with digit
# import my-module         # Hyphens not allowed
# import class             # 'class' is a keyword

Error común: Ocultar módulos de la biblioteca estándar

Ten cuidado de no nombrar tus módulos igual que los módulos de la biblioteca estándar. Si creas un archivo llamado random.py en el directorio de tu proyecto, Python importará tu archivo en lugar del módulo random de la biblioteca estándar, provocando errores confusos:

python
# Tu archivo: random.py
def my_function():
    return 42
 
# Otro archivo en tu proyecto
import random
print(random.randint(1, 6))  # ERROR: tu random.py no tiene randint()

Para evitarlo, comprueba si un nombre ya está usado por la biblioteca estándar antes de crear un módulo con ese nombre. Puedes comprobarlo intentando importarlo en la shell interactiva de Python. Si se importa sin error, ese nombre ya está ocupado.

Qué ocurre durante la importación

Examinemos qué ocurre realmente cuando importas un módulo. Crea un archivo llamado demo_module.py:

python
# demo_module.py
print("Module is being loaded!")
 
def greet():
    print("Hello from demo_module")
 
print("Module loading complete!")

Ahora impórtalo:

python
# test_import.py
print("Before import")
import demo_module
print("After import")
 
demo_module.greet()

Output:

Before import
Module is being loaded!
Module loading complete!
After import
Hello from demo_module

Observa que las sentencias print() en demo_module.py se ejecutan durante la importación. Esto demuestra que importar un módulo ejecuta todo su código de nivel superior. Las definiciones de funciones se almacenan para uso posterior, pero cualquier código fuera de funciones se ejecuta inmediatamente.

Si importas el mismo módulo de nuevo en el mismo programa, los mensajes de carga no aparecerán otra vez:

python
import demo_module  # First import - executes module code
import demo_module  # Second import - uses cached module
import demo_module  # Third import - still uses cached module

Output:

Module is being loaded!
Module loading complete!

El código del módulo se ejecuta solo una vez, sin importar cuántas veces lo importes.

22.2) Diferentes formas de importar: import, from y as

Python ofrece varias formas de importar módulos y su contenido. Cada enfoque tiene implicaciones diferentes sobre cómo accedes a los nombres importados y cómo afectan a tu espacio de nombres.

Sentencia import básica

La sentencia import básica que ya hemos visto carga el módulo completo:

python
import math
 
result = math.sqrt(16)
print(result)  # Output: 4.0
 
pi_value = math.pi
print(pi_value)  # Output: 3.141592653589793

Con este enfoque, siempre usas el nombre del módulo como prefijo. Esto hace que el código sea muy claro: siempre puedes saber de dónde viene un nombre.

Importar nombres específicos con from

A veces solo necesitas uno o dos elementos de un módulo. La sentencia from te permite importar nombres específicos directamente en tu espacio de nombres:

python
from math import sqrt, pi
 
result = sqrt(25)  # No se necesita el prefijo 'math.'
print(result)  # Output: 5.0
 
print(pi)  # Output: 3.141592653589793

Ahora puedes usar sqrt y pi directamente sin el prefijo math.. Esto es conveniente cuando usas estos nombres con frecuencia.

Veamos otro ejemplo con nuestro módulo greetings:

python
# Usar from import
from greetings import say_hello
 
message = say_hello("Diana")  # Acceso directo
print(message)  # Output: Hello, Diana!
 
# Sin embargo, say_goodbye no está disponible porque no la importamos
# say_goodbye("Diana")  # NameError: name 'say_goodbye' is not defined

Puedes importar varios nombres en una sola sentencia:

python
from greetings import say_hello, say_goodbye, default_greeting
 
print(say_hello("Eve"))      # Output: Hello, Eve!
print(say_goodbye("Frank"))  # Output: Goodbye, Frank!
print(default_greeting)      # Output: Welcome

La importación con comodín (y por qué evitarla)

Python permite importar todo de un módulo usando *:

python
from math import *
 
print(sqrt(9))   # Output: 3.0
print(cos(0))    # Output: 1.0
print(pi)        # Output: 3.141592653589793

Esto importa todos los nombres públicos del módulo (nombres que no empiezan con guion bajo). Aunque parece conveniente, en general se considera una mala práctica porque:

  1. Contaminación del espacio de nombres(namespace): No sabes exactamente qué nombres estás importando
  2. Conflictos de nombres: Los nombres importados pueden sobrescribir tus propias variables
  3. Legibilidad: Quien lee el código no puede saber de dónde vienen los nombres
python
# Ejemplo problemático
from math import *
 
# Más adelante en tu código...
def sqrt(x):
    """Your own square root function."""
    return x ** 0.5
 
# ¿Qué sqrt estás usando? ¿La tuya o la de math?
result = sqrt(16)  # Confusing!

Mejor práctica: Importa nombres específicos o usa la sentencia import básica. Evita from module import * excepto en sesiones interactivas donde estás experimentando.

Renombrar importaciones con as

A veces los nombres de módulos o funciones son largos, o quieres evitar conflictos de nombres. La palabra clave as te permite crear un alias:

python
import math as m
 
result = m.sqrt(36)
print(result)  # Output: 6.0

Esto es especialmente útil para módulos con nombres largos o al seguir convenciones comunes:

python
import datetime as dt
 
today = dt.date.today()
print(today)  # Output: 2025-12-19 (or current date)

También puedes renombrar importaciones específicas:

python
from math import sqrt as square_root
 
result = square_root(49)
print(result)  # Output: 7.0

Esto ayuda cuando tienes conflictos de nombres:

python
from math import sqrt as math_sqrt
 
def sqrt(x):
    """Custom square root with input validation."""
    if x < 0:
        return None
    return math_sqrt(x)
 
print(sqrt(25))   # Output: 5.0 (your function)
print(sqrt(-4))   # Output: None (your function)

Combinar estilos de importación

Puedes mezclar diferentes estilos de importación en el mismo archivo:

python
import math
from datetime import date, time
from random import randint as random_int
 
# Usar math con prefijo
radius = 5
area = math.pi * radius ** 2
 
# Usar date y time directamente
today = date.today()
current_time = time(14, 30)
 
# Usar función renombrada
dice_roll = random_int(1, 6)

Elegir el estilo de importación correcto

Aquí tienes una guía de decisión:

Usa import module cuando:

  • Necesitas varios elementos del módulo
  • Quieres la máxima claridad sobre de dónde vienen los nombres
  • El nombre del módulo es corto y claro

Usa from module import name cuando:

  • Solo necesitas uno o dos elementos específicos
  • Los nombres son distintivos y es poco probable que entren en conflicto
  • Usarás los nombres con frecuencia

Usa import module as alias cuando:

  • El nombre del módulo es muy largo
  • Sigues una convención común (como import numpy as np)
  • Necesitas evitar conflictos con otros módulos

Evita from module import * en código de producción:

  • Úsalo solo para experimentos rápidos en la shell interactiva
  • Nunca lo uses en módulos que otros vayan a importar

Veamos un ejemplo completo que demuestra buenas prácticas de importación:

python
# data_processor.py
import math
from statistics import mean, median
from datetime import datetime as dt
 
def calculate_statistics(numbers):
    """Calculate various statistics for a list of numbers."""
    if not numbers:
        return None
    
    avg = mean(numbers)
    mid = median(numbers)
    std_dev = math.sqrt(sum((x - avg) ** 2 for x in numbers) / len(numbers))
    
    return {
        'mean': avg,
        'median': mid,
        'std_dev': std_dev,
        'timestamp': dt.now()
    }
 
# Probar la función
data = [10, 20, 30, 40, 50]
stats = calculate_statistics(data)
print(f"Mean: {stats['mean']}")      # Output: Mean: 30.0
print(f"Median: {stats['median']}")  # Output: Median: 30
print(f"Std Dev: {stats['std_dev']:.2f}")  # Output: Std Dev: 14.14

Este ejemplo muestra:

  • import math para el módulo completo (podríamos usar otras funciones de math más adelante)
  • from statistics import mean, median para funciones específicas que usamos con frecuencia
  • from datetime import datetime as dt para un módulo con alias común

22.3) Visión general de módulos comunes de la biblioteca estándar de Python

Python viene con una rica biblioteca estándar: una colección de módulos que ofrecen soluciones a tareas comunes de programación. Estos módulos siempre están disponibles; no necesitas instalar nada extra. Comprender qué está disponible en la biblioteca estándar te ayuda a evitar “reinventar la rueda”.

El módulo math

El módulo math proporciona funciones matemáticas más allá de la aritmética básica:

python
import math
 
# Funciones trigonométricas
angle_rad = math.radians(45)  # Convert degrees to radians
print(math.sin(angle_rad))    # Output: 0.7071067811865476
print(math.cos(angle_rad))    # Output: 0.7071067811865475
 
# Redondeo y valor absoluto
print(math.ceil(4.2))   # Output: 5 (round up)
print(math.floor(4.8))  # Output: 4 (round down)
print(math.fabs(-7.5))  # Output: 7.5 (absolute value as float)
 
# Exponencial y logarítmico
print(math.exp(2))      # Output: 7.38905609893065 (e^2)
print(math.log(100))    # Output: 4.605170185988092 (natural log)
print(math.log10(100))  # Output: 2.0 (base-10 log)
 
# Constantes
print(math.pi)  # Output: 3.141592653589793
print(math.e)   # Output: 2.718281828459045

Como aprendimos en el Capítulo 4, el módulo math es esencial para operaciones matemáticas avanzadas.

El módulo random

El módulo random genera números pseudoaleatorios y hace selecciones aleatorias:

python
import random
 
# Enteros aleatorios
dice = random.randint(1, 6)  # Random integer from 1 to 6 (inclusive)
print(f"Dice roll: {dice}")
 
# Flotantes aleatorios
probability = random.random()  # Random float from 0.0 to 1.0
print(f"Probability: {probability:.4f}")
 
# Elección aleatoria de una secuencia
colors = ['red', 'blue', 'green', 'yellow']
chosen_color = random.choice(colors)
print(f"Chosen color: {chosen_color}")
 
# Barajar una lista in place
deck = ['A', 'K', 'Q', 'J', '10']
random.shuffle(deck)
print(f"Shuffled deck: {deck}")
 
# Muestra aleatoria sin reemplazo
lottery_numbers = random.sample(range(1, 50), 6)
print(f"Lottery numbers: {sorted(lottery_numbers)}")

Salida (ejemplo: variará debido a la aleatoriedad):

Dice roll: 4
Probability: 0.7382
Chosen color: green
Shuffled deck: ['Q', 'A', '10', 'K', 'J']
Lottery numbers: [7, 15, 23, 31, 38, 42]

El módulo datetime

El módulo datetime maneja fechas y horas:

python
from datetime import date, time, datetime, timedelta
 
# Fecha y hora actuales
today = date.today()
now = datetime.now()
print(f"Today: {today}")  # Output: Today: 2025-12-19
print(f"Now: {now}")      # Output: Now: 2025-12-19 14:30:45.123456
 
# Crear fechas y horas específicas
birthday = date(1990, 5, 15)
meeting_time = time(14, 30)
appointment = datetime(2025, 12, 25, 10, 0)
 
print(f"Birthday: {birthday}")          # Output: Birthday: 1990-05-15
print(f"Meeting: {meeting_time}")       # Output: Meeting: 14:30:00
print(f"Appointment: {appointment}")    # Output: Appointment: 2025-12-25 10:00:00
 
# Aritmética de fechas con timedelta
tomorrow = today + timedelta(days=1)
next_week = today + timedelta(weeks=1)
print(f"Tomorrow: {tomorrow}")    # Output: Tomorrow: 2025-12-20
print(f"Next week: {next_week}")  # Output: Next week: 2025-12-26
 
# Extraer componentes
print(f"Year: {today.year}")      # Output: Year: 2025
print(f"Month: {today.month}")    # Output: Month: 12
print(f"Day: {today.day}")        # Output: Day: 19

El módulo os

El módulo os proporciona funcionalidad del sistema operativo. Exploraremos esto en detalle en el Capítulo 26, pero aquí tienes un adelanto:

python
import os
 
# Directorio de trabajo actual
current_dir = os.getcwd()
print(f"Current directory: {current_dir}")
 
# Listar archivos en un directorio
files = os.listdir('.')
print(f"Files: {files[:3]}")  # Show first 3 files
 
# Comprobar si existe una ruta
exists = os.path.exists('myfile.txt')
print(f"File exists: {exists}")
 
# Unir componentes de ruta (funciona en distintos sistemas operativos)
file_path = os.path.join('data', 'users', 'profile.txt')
print(f"Path: {file_path}")  # Output: data/users/profile.txt (or data\users\profile.txt on Windows)

El módulo sys

El módulo sys proporciona parámetros y funciones específicos del sistema:

python
import sys
 
# Información de versión de Python
print(f"Python version: {sys.version}")
print(f"Version info: {sys.version_info}")
 
# Información de plataforma
print(f"Platform: {sys.platform}")  # Output: linux, darwin, win32, etc.
 
# Tamaño máximo de entero
print(f"Max int: {sys.maxsize}")

El módulo statistics

El módulo statistics proporciona funciones para cálculos estadísticos:

python
import statistics
 
grades = [85, 92, 78, 90, 88, 95, 82]
 
# Tendencia central
avg = statistics.mean(grades)
mid = statistics.median(grades)
mode_val = statistics.mode([1, 2, 2, 3, 3, 3, 4])
 
print(f"Mean: {avg}")      # Output: Mean: 87.14285714285714
print(f"Median: {mid}")    # Output: Median: 88
print(f"Mode: {mode_val}") # Output: Mode: 3
 
# Dispersión
std_dev = statistics.stdev(grades)
variance = statistics.variance(grades)
 
print(f"Standard deviation: {std_dev:.2f}")  # Output: Standard deviation: 5.90
print(f"Variance: {variance:.2f}")           # Output: Variance: 34.81

El módulo collections

El módulo collections proporciona tipos de contenedores especializados. Exploraremos esto más en el Capítulo 39, pero aquí tienes una muestra:

python
from collections import Counter, defaultdict
 
# Counter - contar ocurrencias
text = "hello world"
letter_counts = Counter(text)
print(letter_counts)  # Output: Counter({'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1})
print(letter_counts['l'])  # Output: 3
 
# defaultdict - diccionario con valores por defecto
word_lists = defaultdict(list)
word_lists['fruits'].append('apple')
word_lists['fruits'].append('banana')
word_lists['vegetables'].append('carrot')
print(dict(word_lists))  # Output: {'fruits': ['apple', 'banana'], 'vegetables': ['carrot']}

Encontrar más módulos de la biblioteca estándar

La biblioteca estándar de Python contiene más de 200 módulos. Puedes explorarlos de varias maneras:

python
# Ver todos los módulos disponibles (esto tarda un momento)
help('modules')
 
# Obtener ayuda de un módulo específico
import math
help(math)
 
# Ver qué hay en un módulo
import random
print(dir(random))

La documentación de Python (https://docs.python.org/3/library/) ofrece información completa sobre cada módulo de la biblioteca estándar. A medida que ganes experiencia, descubrirás qué módulos son más útiles para tu trabajo.

22.4) Crear y usar tus propios módulos

Crear tus propios módulos es sencillo: cualquier archivo de Python puede ser un módulo. La clave es organizar tu código de forma cuidadosa para que los módulos sean enfocados, reutilizables y fáciles de entender.

Crear un módulo simple

Vamos a crear un módulo para trabajar con calificaciones de estudiantes. Crea un archivo llamado grade_calculator.py:

python
# grade_calculator.py
"""Module for calculating and analyzing student grades."""
 
def calculate_average(grades):
    """Calculate the average of a list of grades."""
    if not grades:
        return 0
    return sum(grades) / len(grades)
 
def get_letter_grade(numeric_grade):
    """Convert a numeric grade to a letter grade."""
    if numeric_grade >= 90:
        return 'A'
    elif numeric_grade >= 80:
        return 'B'
    elif numeric_grade >= 70:
        return 'C'
    elif numeric_grade >= 60:
        return 'D'
    else:
        return 'F'
 
def find_highest(grades):
    """Find the highest grade in a list."""
    if not grades:
        return None
    return max(grades)
 
def find_lowest(grades):
    """Find the lowest grade in a list."""
    if not grades:
        return None
    return min(grades)
 
# Constantes a nivel de módulo
PASSING_GRADE = 60
HONOR_ROLL_THRESHOLD = 90

Ahora crea otro archivo para usar este módulo:

python
# student_report.py
import grade_calculator
 
# Puntuaciones de exámenes del estudiante
test_scores = [85, 92, 78, 88, 95]
 
# Calcular estadísticas
average = grade_calculator.calculate_average(test_scores)
letter = grade_calculator.get_letter_grade(average)
highest = grade_calculator.find_highest(test_scores)
lowest = grade_calculator.find_lowest(test_scores)
 
# Generar informe
print("Student Grade Report")
print("=" * 40)
print(f"Test Scores: {test_scores}")
print(f"Average: {average:.1f}")
print(f"Letter Grade: {letter}")
print(f"Highest Score: {highest}")
print(f"Lowest Score: {lowest}")
 
# Comprobar si está en el cuadro de honor
if average >= grade_calculator.HONOR_ROLL_THRESHOLD:
    print("Status: HONOR ROLL!")
elif average >= grade_calculator.PASSING_GRADE:
    print("Status: Passing")
else:
    print("Status: Needs Improvement")

Output:

Student Grade Report
========================================
Test Scores: [85, 92, 78, 88, 95]
Average: 87.6
Letter Grade: B
Highest Score: 95
Lowest Score: 78
Status: Passing

Documentación del módulo

Fíjate en el docstring al inicio de grade_calculator.py. Este docstring a nivel de módulo describe lo que hace el módulo. Aparece cuando alguien usa help():

python
import grade_calculator
help(grade_calculator)

Esto muestra la documentación del módulo, incluyendo el docstring del módulo y todos los docstrings de las funciones. Una buena documentación hace que tus módulos sean más fáciles de usar.

Variables y constantes a nivel de módulo

Los módulos pueden contener variables que se comparten en todos los usos del módulo. A menudo se usan para configuración o constantes:

python
# config.py
"""Application configuration settings."""
 
# Configuración de base de datos
DB_HOST = "localhost"
DB_PORT = 5432
DB_NAME = "myapp"
 
# Configuración de la aplicación
MAX_LOGIN_ATTEMPTS = 3
SESSION_TIMEOUT = 1800  # segundos
DEBUG_MODE = False
 
# Rutas de archivos
DATA_DIR = "/var/data"
LOG_DIR = "/var/log"
 
# Indicadores de funcionalidades
ENABLE_CACHING = True
ENABLE_LOGGING = True

Usar configuración desde un módulo:

python
# app.py
import config
 
def connect_database():
    """Connect to the database using config settings."""
    print(f"Connecting to {config.DB_HOST}:{config.DB_PORT}")
    print(f"Database: {config.DB_NAME}")
    
    if config.DEBUG_MODE:
        print("DEBUG: Connection details logged")
 
def check_login_attempts(attempts):
    """Check if login attempts exceed the limit."""
    if attempts >= config.MAX_LOGIN_ATTEMPTS:
        print(f"Too many attempts! Maximum is {config.MAX_LOGIN_ATTEMPTS}")
        return False
    return True
 
connect_database()
print(check_login_attempts(2))  # Output: True
print(check_login_attempts(4))  # Output: Too many attempts! Maximum is 3

Output:

Connecting to localhost:5432
Database: myapp
True
Too many attempts! Maximum is 3

Importante: Las variables a nivel de módulo se comparten en todas las importaciones. Si modificas una variable del módulo, el cambio afecta a todo el código que use ese módulo:

python
# file1.py
import config
config.DEBUG_MODE = True
print(f"File1 - Debug mode: {config.DEBUG_MODE}")
 
# file2.py
import config
print(f"File2 - Debug mode: {config.DEBUG_MODE}")  # Output: Will be True!

Este comportamiento puede ser útil pero también sorprendente. Ten cuidado al modificar variables a nivel de módulo.

Nombres privados en módulos

Por convención, los nombres que empiezan con guion bajo se consideran privados o internos al módulo:

python
# user_manager.py
"""Module for managing user accounts."""
 
# Función auxiliar privada
def _validate_email(email):
    """Internal function to validate email format."""
    return '@' in email and '.' in email
 
# Función pública
def create_user(username, email):
    """Create a new user account."""
    if not _validate_email(email):
        return None
    
    user = {
        'username': username,
        'email': email,
        'active': True
    }
    return user
 
# Constante privada
_MAX_USERNAME_LENGTH = 20
 
# Constante pública
MIN_PASSWORD_LENGTH = 8

Cuando usas from user_manager import *, los nombres privados (los que empiezan con guion bajo) no se importan. Sin embargo, aún puedes acceder a ellos explícitamente si lo necesitas:

python
import user_manager
 
# Función pública - pensada para usarse
user = user_manager.create_user("alice", "alice@example.com")
 
# Función privada - se puede acceder pero no deberías depender de ella
# (podría cambiar en versiones futuras)
is_valid = user_manager._validate_email("test@test.com")

El prefijo con guion bajo es una señal para otros programadores: “Esto es un detalle de implementación. No dependas de que se mantenga igual”.

22.5) Comprender los paquetes y __init__.py

A medida que los proyectos crecen, querrás organizar varios módulos relacionados en un paquete. Un paquete es un directorio que contiene módulos de Python y un archivo especial __init__.py.

¿Qué es un paquete?

Un paquete es una forma de organizar múltiples módulos en una estructura jerárquica. Piensa en ello como una carpeta que contiene archivos de Python, donde la carpeta en sí puede importarse.

Aquí tienes una estructura simple de paquetes:

myproject/
    main.py
    utilities/
        __init__.py
        text.py
        math.py
        file.py

En esta estructura, utilities es un paquete que contiene tres módulos: text, math y file. El archivo __init__.py (que puede estar vacío) le dice a Python que utilities es un paquete.

Crear un paquete simple

Vamos a crear un paquete para procesamiento de datos. Primero, crea esta estructura de directorios:

data_tools/
    __init__.py
    validators.py
    formatters.py

Crea validators.py:

python
# data_tools/validators.py
"""Data validation functions."""
 
def is_valid_email(email):
    """Check if email has basic valid format."""
    return '@' in email and '.' in email.split('@')[1]
 
def is_valid_phone(phone):
    """Check if phone number has valid format (simple check)."""
    digits = ''.join(c for c in phone if c.isdigit())
    return len(digits) == 10
 
def is_positive_number(value):
    """Check if value is a positive number."""
    try:
        return float(value) > 0
    except (ValueError, TypeError):
        return False

Crea formatters.py:

python
# data_tools/formatters.py
"""Data formatting functions."""
 
def format_phone(phone):
    """Format phone number as (XXX) XXX-XXXX."""
    digits = ''.join(c for c in phone if c.isdigit())
    if len(digits) != 10:
        return phone
    return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"
 
def format_currency(amount):
    """Format number as currency."""
    return f"${amount:,.2f}"
 
def format_percentage(value, decimals=1):
    """Format number as percentage."""
    return f"{value * 100:.{decimals}f}%"

Crea un __init__.py vacío:

python
# data_tools/__init__.py
"""Data processing tools package."""

Importar desde paquetes

Ahora puedes importar desde el paquete de varias maneras:

python
# Método 1: Importar el módulo desde el paquete
import data_tools.validators
 
email = "user@example.com"
is_valid = data_tools.validators.is_valid_email(email)
print(f"Email valid: {is_valid}")  # Output: Email valid: True
 
# Método 2: Importar un módulo específico con from
from data_tools import formatters
 
phone = "1234567890"
formatted = formatters.format_phone(phone)
print(f"Formatted phone: {formatted}")  # Output: Formatted phone: (123) 456-7890
 
# Método 3: Importar funciones específicas
from data_tools.validators import is_valid_phone
from data_tools.formatters import format_currency
 
print(is_valid_phone("555-1234"))  # Output: False (not 10 digits)
print(format_currency(1234.56))    # Output: $1,234.56

El archivo __init__.py

El archivo __init__.py sirve para dos propósitos:

  1. Marca el directorio como un paquete: Python reconoce los directorios con __init__.py como paquetes
  2. Código de inicialización del paquete: El código en __init__.py se ejecuta cuando el paquete se importa por primera vez

El archivo __init__.py puede estar vacío, pero también puede contener código para que el paquete sea más fácil de usar:

python
# data_tools/__init__.py
"""Data processing tools package."""
 
# Importar funciones usadas comúnmente al espacio de nombres del paquete
from data_tools.validators import is_valid_email, is_valid_phone
from data_tools.formatters import format_phone, format_currency
 
# Versión del paquete
__version__ = '1.0.0'
 
# Constante a nivel de paquete
DEFAULT_CURRENCY_SYMBOL = '$'

Ahora, quien lo use puede importar directamente desde el paquete:

python
# Instead of: from data_tools.validators import is_valid_email
# You can write:
from data_tools import is_valid_email, format_currency
 
print(is_valid_email("test@test.com"))  # Output: True
print(format_currency(99.99))           # Output: $99.99

22.6) La variable __name__ y if __name__ == "__main__": Ejecutar un archivo como script

Los archivos de Python pueden cumplir dos propósitos: se pueden importar como módulos o ejecutar como scripts independientes. La variable especial __name__ te ayuda a escribir código que funcione bien en ambas situaciones.

Comprender __name__

Cada módulo de Python tiene una variable integrada llamada __name__. Python establece esta variable de forma distinta dependiendo de cómo se esté usando el archivo:

  • Cuando se importa: __name__ se establece en el nombre del módulo
  • Cuando se ejecuta directamente: __name__ se establece en "__main__"

Veamos esto en acción. Crea un archivo llamado demo_name.py:

python
# demo_name.py
print(f"The __name__ variable is: {__name__}")

Ahora ejecútalo directamente:

bash
python demo_name.py

Output:

The __name__ variable is: __main__

Ahora impórtalo desde otro archivo:

python
# test_import.py
import demo_name

Output:

The __name__ variable is: demo_name

Cuando ejecutas demo_name.py directamente, Python establece __name__ en "__main__". Cuando lo importas, Python establece __name__ en el nombre del módulo ("demo_name").

El patrón if __name__ == "__main__":

Este comportamiento te permite escribir código que solo se ejecuta cuando el archivo se ejecuta directamente, no cuando se importa. Esto se hace con el patrón:

python
if __name__ == "__main__":
    # El código aquí se ejecuta solo cuando el archivo se ejecuta directamente
    pass

Aquí tienes por qué esto es útil. Crea math_utils.py:

python
# math_utils.py
"""Utility functions for mathematical operations."""
 
def calculate_area(radius):
    """Calculate the area of a circle."""
    return 3.14159 * radius ** 2
 
def calculate_circumference(radius):
    """Calculate the circumference of a circle."""
    return 2 * 3.14159 * radius
 
# Código de prueba: se ejecuta solo cuando el archivo se ejecuta directamente
if __name__ == "__main__":
    print("Testing math_utils functions...")
    
    test_radius = 5
    area = calculate_area(test_radius)
    circumference = calculate_circumference(test_radius)
    
    print(f"Radius: {test_radius}")
    print(f"Area: {area:.2f}")
    print(f"Circumference: {circumference:.2f}")

Cuando ejecutas este archivo directamente:

bash
python math_utils.py

Output:

Testing math_utils functions...
Radius: 5
Area: 78.54
Circumference: 31.42

Pero cuando lo importas:

python
# use_math_utils.py
import math_utils
# ¡El código de prueba no se ejecuta!
 
area = math_utils.calculate_area(10)
print(f"Area of circle: {area:.2f}")  # Output: Area of circle: 314.16

El código de prueba en el bloque if __name__ == "__main__": no se ejecuta durante la importación. Esto te permite incluir código de prueba, ejemplos o demostraciones en tus módulos sin afectar al código que los importa.

Usos comunes de if __name__ == "__main__":

Pruebas y demostraciones

Incluye ejemplos que muestren cómo usar tu módulo:

python
# string_tools.py
def reverse_string(text):
    """Reverse a string."""
    return text[::-1]
 
def count_vowels(text):
    """Count vowels in text."""
    vowels = 'aeiouAEIOU'
    return sum(1 for char in text if char in vowels)
 
if __name__ == "__main__":
    # Código de demostración
    sample = "Hello, World!"
    
    print(f"Original: {sample}")
    print(f"Reversed: {reverse_string(sample)}")
    print(f"Vowels: {count_vowels(sample)}")

En este capítulo, hemos aprendido a organizar código Python usando módulos y paquetes. Exploramos cómo funciona el sistema de importación, diferentes formas de importar código y cómo crear nuestros propios módulos y paquetes. También aprendimos sobre la variable __name__ y el patrón if __name__ == "__main__": que permite que los archivos funcionen tanto como módulos importables como scripts independientes.

Estas herramientas de organización se vuelven cada vez más importantes a medida que tus programas crecen. En el próximo capítulo, exploraremos cómo usar funciones como datos y aplicar técnicas simples de programación funcional, basándonos en la base sólida de organización de código que hemos establecido aquí.


© 2025. Primesoft Co., Ltd.
support@primesoft.ai