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:
# 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:
# 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: WelcomeCuando ejecutas main.py, Python ejecuta la sentencia import greetings. Esto es lo que ocurre entre bambalinas:
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.
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óduloLa 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:
- Directorio actual: El directorio que contiene el script que estás ejecutando
- PYTHONPATH: Directorios listados en la variable de entorno
PYTHONPATH(si está configurada) - Biblioteca estándar: Directorios de módulos integrados de Python
- Site-Packages: Paquetes de terceros instalados con pip
Puedes ver la ruta de búsqueda de Python examinando sys.path:
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-packagesLa 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
# 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 keywordError 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:
# 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:
# demo_module.py
print("Module is being loaded!")
def greet():
print("Hello from demo_module")
print("Module loading complete!")Ahora impórtalo:
# 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_moduleObserva 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:
import demo_module # First import - executes module code
import demo_module # Second import - uses cached module
import demo_module # Third import - still uses cached moduleOutput:
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:
import math
result = math.sqrt(16)
print(result) # Output: 4.0
pi_value = math.pi
print(pi_value) # Output: 3.141592653589793Con 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:
from math import sqrt, pi
result = sqrt(25) # No se necesita el prefijo 'math.'
print(result) # Output: 5.0
print(pi) # Output: 3.141592653589793Ahora 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:
# 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 definedPuedes importar varios nombres en una sola sentencia:
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: WelcomeLa importación con comodín (y por qué evitarla)
Python permite importar todo de un módulo usando *:
from math import *
print(sqrt(9)) # Output: 3.0
print(cos(0)) # Output: 1.0
print(pi) # Output: 3.141592653589793Esto 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:
- Contaminación del espacio de nombres(namespace): No sabes exactamente qué nombres estás importando
- Conflictos de nombres: Los nombres importados pueden sobrescribir tus propias variables
- Legibilidad: Quien lee el código no puede saber de dónde vienen los nombres
# 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:
import math as m
result = m.sqrt(36)
print(result) # Output: 6.0Esto es especialmente útil para módulos con nombres largos o al seguir convenciones comunes:
import datetime as dt
today = dt.date.today()
print(today) # Output: 2025-12-19 (or current date)También puedes renombrar importaciones específicas:
from math import sqrt as square_root
result = square_root(49)
print(result) # Output: 7.0Esto ayuda cuando tienes conflictos de nombres:
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:
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:
# 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.14Este ejemplo muestra:
import mathpara el módulo completo (podríamos usar otras funciones de math más adelante)from statistics import mean, medianpara funciones específicas que usamos con frecuenciafrom datetime import datetime as dtpara 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:
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.718281828459045Como 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:
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:
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: 19El 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:
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:
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:
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.81El 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:
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:
# 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:
# 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 = 90Ahora crea otro archivo para usar este módulo:
# 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: PassingDocumentació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():
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:
# 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 = TrueUsar configuración desde un módulo:
# 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 3Output:
Connecting to localhost:5432
Database: myapp
True
Too many attempts! Maximum is 3Importante: 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:
# 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:
# 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 = 8Cuando 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:
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.pyEn 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.pyCrea validators.py:
# 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 FalseCrea formatters.py:
# 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:
# data_tools/__init__.py
"""Data processing tools package."""Importar desde paquetes
Ahora puedes importar desde el paquete de varias maneras:
# 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.56El archivo __init__.py
El archivo __init__.py sirve para dos propósitos:
- Marca el directorio como un paquete: Python reconoce los directorios con
__init__.pycomo paquetes - Código de inicialización del paquete: El código en
__init__.pyse 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:
# 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:
# 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.9922.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:
# demo_name.py
print(f"The __name__ variable is: {__name__}")Ahora ejecútalo directamente:
python demo_name.pyOutput:
The __name__ variable is: __main__Ahora impórtalo desde otro archivo:
# test_import.py
import demo_nameOutput:
The __name__ variable is: demo_nameCuando 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:
if __name__ == "__main__":
# El código aquí se ejecuta solo cuando el archivo se ejecuta directamente
passAquí tienes por qué esto es útil. Crea math_utils.py:
# 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:
python math_utils.pyOutput:
Testing math_utils functions...
Radius: 5
Area: 78.54
Circumference: 31.42Pero cuando lo importas:
# 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.16El 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:
# 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í.