40. Escribir código limpio y legible
A lo largo de este libro, has aprendido la sintaxis de Python, estructuras de datos, flujo de control, funciones y clases, y muchos otros conceptos de programación. Ahora puedes escribir programas que funcionan. Pero hay una diferencia crucial entre el código que funciona y el código que es mantenible—código que tú y otros pueden entender, modificar y depurar meses o años después.
Este capítulo se centra en escribir código limpio y legible. Aprenderás las convenciones y prácticas que hacen que el código Python sea profesional y mantenible. Estas no son solo reglas arbitrarias—son pautas probadas en batalla que facilitan la colaboración, reducen errores y te ayudan a entender tu propio código cuando vuelves a él más tarde.
40.1) Por qué importa el estilo: leer vs. escribir código
40.1.1) El código se lee más a menudo de lo que se escribe
Cuando escribes código, pasas minutos u horas creándolo. Pero ese código se leerá muchas veces: cuando lo depures, cuando añadas funciones, cuando otros desarrolladores trabajen con él y cuando vuelvas a él meses después tratando de recordar qué hace.
Considera este código que funciona pero tiene un estilo deficiente:
# ADVERTENCIA: Estilo deficiente - solo para demostración
def c(l):
t=0
for i in l:
t=t+i
return t/len(l)
data=[85,92,78,90,88]
result=c(data)
print(result) # Output: 86.6Este código funciona perfectamente. Calcula el promedio de una lista de números. Pero entender qué hace requiere un análisis cuidadoso. Ahora compáralo con esta versión:
def calculate_average(numbers):
"""Calculate the arithmetic mean of a list of numbers."""
total = 0
for number in numbers:
total = total + number
return total / len(numbers)
test_scores = [85, 92, 78, 90, 88]
average_score = calculate_average(test_scores)
print(average_score) # Output: 86.6¿Qué hace mejor a la segunda versión?
- El nombre de la función (
calculate_average) indica claramente el propósito - Los nombres de variables (
numbers,total,test_scores) son descriptivos - La docstring explica qué hace la función
- El espaciado adecuado hace que la estructura sea clara
- Cualquiera puede entender este código sin estudiarlo
Ambas versiones producen resultados idénticos, pero la segunda versión es inmediatamente comprensible.
La idea clave: escribes código una vez, pero lo lees decenas o cientos de veces. Invertir unos segundos extra en nombres claros y formato te ahorra horas de confusión más tarde.
40.1.2) La legibilidad reduce errores
El código claro es más fácil de depurar porque puedes entender rápidamente qué hace cada parte. Cuando los nombres de variables son descriptivos y la estructura es limpia, puedes detectar errores de lógica con más facilidad.
# Difícil de depurar: ¿qué representan estas variables?
# ADVERTENCIA: Estilo deficiente - solo para demostración
def process(x, y):
if x > y:
return x * (1 - y)
return x
result = process(100, 0.1)# Fácil de depurar: es claro lo que está pasando
def apply_discount(price, discount_rate):
"""Calculate price after applying discount rate (0.0 to 1.0)."""
discount_amount = price * discount_rate
final_price = price - discount_amount
return final_price
original_price = 100
discount = 0.1 # 10% discount
final_price = apply_discount(original_price, discount)
print(f"Final price: ${final_price}")
# Output: Final price: $90.0En la segunda versión, puedes ver inmediatamente la lógica: "Estamos calculando un monto de descuento y luego restándolo del precio". En la primera versión, tienes que seguir mentalmente qué representan x e y y averiguar qué significa x * (1 - y).
40.1.3) La consistencia permite la colaboración
Cuando todos en un equipo siguen las mismas convenciones de estilo, el código se vuelve predecible. No malgastas energía mental descifrando estilos de formato distintos—puedes concentrarte en entender la lógica.
Python tiene una guía de estilo oficial llamada PEP 8 (Python Enhancement Proposal 8). PEP 8 define convenciones para:
- Cómo nombrar variables, funciones y clases
- Cómo dar formato al código (espaciado, longitud de línea, sangría)
- Cuándo usar comentarios y docstrings
- Cómo organizar las importaciones
Seguir PEP 8 significa que tu código se verá familiar para otros programadores de Python, haciendo que la colaboración sea más fluida. Cubriremos las pautas esenciales de PEP 8 en las siguientes secciones.
40.2) Convenciones de nombres: variables, funciones y clases (PEP 8)
40.2.1) Principios generales de nomenclatura
Los buenos nombres son descriptivos y no ambiguos. Deben decirte qué representa o hace algo sin obligarte a leer la implementación.
Principios clave:
- Usa palabras completas, no abreviaturas (excepto las muy comunes como
id,url,html) - Sé específico:
user_countes mejor quecount,calculate_total_pricees mejor quecalculate - Evita nombres de una sola letra excepto para bucles muy cortos o fórmulas matemáticas
- No incluyas información de tipo en los nombres (Python es de tipado dinámico)
# Nombres deficientes: no queda claro qué representan
# ADVERTENCIA: Estilo deficiente - solo para demostración
# ¿Qué es 'n'? ¿Un número? ¿Un nombre? ¿Un nodo?
# ¿Qué es 'd'? ¿Una fecha? ¿Una distancia? ¿Una duración?
# ¿Qué es 'l'? ¡Parece el número 1!
n = "Alice"
d = 25
l = [1, 2, 3]
calc = lambda x: x * 2
# Buenos nombres: claros y descriptivos
student_name = "Alice"
age_in_years = 25
test_scores = [1, 2, 3]
double_value = lambda x: x * 2Excepción: variables cortas en bucles
# Aceptable: muy corto, contexto claro
for i in range(10):
print(i)
for x, y in coordinates:
distance = (x**2 + y**2) ** 0.5
# Pero prefiere nombres descriptivos para mayor claridad
for student_index in range(len(students)):
print(students[student_index])
for point_x, point_y in coordinates:
distance = (point_x**2 + point_y**2) ** 0.540.2.2) Nombres de variables y funciones: snake_case
En Python, las variables y funciones usan snake_case: todo en minúsculas con palabras separadas por guiones bajos.
# Variables
user_name = "Bob"
total_price = 99.99
is_valid = True
max_retry_count = 3
# Functions
def calculate_tax(amount, rate):
"""Calculate tax on a given amount."""
return amount * rate
def send_email_notification(recipient, message):
"""Send an email to the specified recipient."""
print(f"Sending to {recipient}: {message}")
# Using the functions
tax_amount = calculate_tax(100, 0.08)
send_email_notification("user@example.com", "Welcome!")¿Por qué snake_case? Es muy legible. Los guiones bajos crean límites claros entre palabras, haciendo que los nombres sean fáciles de escanear. Compara calculatetotalprice (difícil de leer) con calculate_total_price (clarísimo de inmediato).
40.2.3) Nombres de constantes: UPPER_SNAKE_CASE
Las constantes—valores que no deberían cambiar durante la ejecución del programa—usan UPPER_SNAKE_CASE: todo en mayúsculas con guiones bajos.
# Constantes a nivel de módulo
MAX_LOGIN_ATTEMPTS = 3
DEFAULT_TIMEOUT_SECONDS = 30
PI = 3.14159
DATABASE_URL = "postgresql://localhost/mydb"
def validate_password_length(password):
"""Check if password meets minimum length requirement."""
MIN_PASSWORD_LENGTH = 8 # Constante dentro de la función
return len(password) >= MIN_PASSWORD_LENGTH
# Usar constantes
if login_attempts > MAX_LOGIN_ATTEMPTS:
print("Account locked")Importante: Python no tiene sintaxis integrada para constantes. A diferencia de algunos lenguajes (como const en JavaScript o final en Java), Python no tiene una forma de declarar que una variable no pueda cambiarse.
En su lugar, los programadores de Python usan una convención de nombres para indicar la intención:
UPPER_SNAKE_CASEsignifica: "Pretendo que esto sea una constante—no lo modifiques"- Es una herramienta de comunicación entre programadores, no una característica del lenguaje
# Python no tiene sintaxis de constantes: esto es solo una variable normal
MAX_LOGIN_ATTEMPTS = 3
# Python no te impedirá modificarla
MAX_LOGIN_ATTEMPTS = 5 # ❌ Técnicamente funciona, pero viola la convención
# La convención de nombres es una señal sobre la INTENCIÓN:
# "Nombré esto en MAYÚSCULAS para mostrar que no quiero que cambie"Buena práctica: si un valor realmente necesita cambiar durante la ejecución del programa, no lo nombres como constante:
# Este valor cambiará: usa minúsculas
max_login_attempts = 3
max_login_attempts = 5 # ✅ OK: el nombre indica que puede cambiar
# Este valor no debería cambiar nunca: usa MAYÚSCULAS
MAX_LOGIN_ATTEMPTS = 3
# No lo reasignes más tarde en el códigoLa convención ayuda a los programadores a entender tu intención y evitar errores. Cuando ves MAX_LOGIN_ATTEMPTS, sabes que no debes modificarlo.
40.2.4) Nombres de clases: PascalCase
Los nombres de clases usan PascalCase (también llamado CapWords): cada palabra empieza con mayúscula, sin guiones bajos.
# Definiciones de clases
class Student:
"""Represent a student with name and grades."""
def __init__(self, name):
self.name = name
self.grades = []
class ShoppingCart:
"""Manage items in a shopping cart."""
def __init__(self):
self.items = []
def add_item(self, item):
"""Add an item to the cart."""
self.items.append(item)
class DatabaseConnection:
"""Handle database connection and queries."""
def __init__(self, url):
self.url = url
# Creating instances (note: instances use snake_case variable names)
student = Student("Alice")
shopping_cart = ShoppingCart()
db_connection = DatabaseConnection("localhost")¿Por qué PascalCase para las clases? Distingue visualmente las clases de las funciones y las variables. Cuando ves Student(), sabes de inmediato que está creando una instancia de una clase. Cuando ves calculate_average(), sabes que está llamando a una función.
40.2.5) Nombres privados e internos: guion bajo inicial
Los nombres que empiezan con un solo guion bajo (_name) indican uso interno—están pensados para usarse dentro del módulo o la clase, no por código externo.
Python no tiene sintaxis para marcar métodos o atributos como "privados" (a diferencia de private en Java o C++). En su lugar, Python usa una convención de nombres con un guion bajo inicial (_name) para comunicar la intención.
Qué significa _name:
- "Esto es solo para uso interno"
- "Esto está pensado para usarse dentro de esta clase o módulo, no para código externo"
- "Esto podría cambiar en cualquier momento en versiones futuras—no dependas de ello"
class BankAccount:
"""Represent a bank account with balance tracking."""
def __init__(self, account_number, initial_balance):
self.account_number = account_number
self._balance = initial_balance # Atributo interno
def deposit(self, amount):
"""Add money to the account."""
if self._validate_amount(amount): # Método interno
self._balance += amount
def _validate_amount(self, amount):
"""Internal helper to validate transaction amounts."""
return amount > 0
def get_balance(self):
"""Return the current balance."""
return self._balance
# Usar la clase
account = BankAccount("12345", 1000)
account.deposit(500)
print(account.get_balance()) # Output: 1500
# Técnicamente funciona, pero viola la convención
print(account._balance)
# Output: 1500 (works, but you shouldn't do this!)
# Técnicamente funciona, pero viola la convención
result = account._validate_amount(100)
# Output: True (works, but you shouldn't do this!)Punto clave: Python no puede impedir que accedas a _balance o que llames a _validate_amount(). El guion bajo es una señal entre programadores, no una característica de seguridad.
Por qué existe esta convención
Como Python no puede imponer privacidad, el guion bajo es cómo los autores de clases comunican su intención:
Lo que señala el guion bajo:
- "Esto es implementación interna—podría cambiar en versiones futuras"
- "Usa los métodos públicos en su lugar—se garantiza que permanecerán estables"
- "Si dependes de detalles internos, tu código podría romperse cuando actualice la librería"
La convención crea un contrato: los autores de clases pueden cambiar libremente la implementación interna (cualquier cosa con _), pero deben mantener estable la interfaz pública. Esto permite que las librerías evolucionen sin romper el código de los usuarios.
40.2.6) Nombres especiales: dobles guiones bajos
Los nombres con doble guion bajo al inicio y al final (__name__) son métodos especiales o métodos mágicos definidos por Python. No crees tus propios nombres con este patrón—está reservado para el uso de Python.
class Point:
"""Represent a 2D point."""
def __init__(self, x, y): # Método especial: inicialización
self.x = x
self.y = y
def __str__(self): # Método especial: representación en string
return f"Point({self.x}, {self.y})"
def __add__(self, other): # Método especial: operador de suma
return Point(self.x + other.x, self.y + other.y)
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1) # Output: Point(1, 2)
print(p1 + p2) # Output: Point(4, 6)Como aprendimos en el Capítulo 31, estos métodos especiales permiten la sobrecarga de operadores y la integración con las funciones integradas de Python.
40.2.7) Tabla resumen de nombres
| Tipo | Convención | Ejemplo |
|---|---|---|
| Variables | snake_case | user_name, total_count |
| Funciones | snake_case | calculate_tax(), send_email() |
| Constantes | UPPER_SNAKE_CASE | MAX_SIZE, DEFAULT_TIMEOUT |
| Clases | PascalCase | Student, ShoppingCart |
| Interno/Privado | _leading_underscore | _balance, _validate() |
| Especial/Mágico | double_underscore | __init__, __str__ |
40.3) Diseño del código: sangría, espaciado y líneas en blanco
40.3.1) Sangría: cuatro espacios
Python usa la sangría para definir bloques de código. Usa siempre 4 espacios por nivel de sangría—nunca tabulaciones, y nunca mezcles tabulaciones y espacios.
def calculate_grade(score):
"""Determine letter grade from numeric score."""
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
else:
return "F"
# Sangría anidada: 4 espacios por nivel
def process_students(students):
"""Process a list of student records."""
for student in students:
if student["active"]:
grade = calculate_grade(student["score"])
print(f"{student['name']}: {grade}")
students = [
{"name": "Alice", "score": 92, "active": True},
{"name": "Bob", "score": 78, "active": True}
]
process_students(students)
# Output:
# Alice: A
# Bob: C¿Por qué 4 espacios? Es el estándar de la comunidad Python. La mayoría del código Python que encuentras usa 4 espacios, así que seguir esta convención hace que tu código sea consistente con el ecosistema.
Configurar tu editor: Los editores de código modernos se pueden configurar para insertar 4 espacios cuando presionas Tab. Esto te da la comodidad de la tecla Tab manteniendo el estándar de 4 espacios.
40.3.2) Longitud máxima de línea: 79 caracteres
PEP 8 recomienda limitar las líneas a 79 caracteres (con hasta 99 caracteres para docstrings y comentarios). Esto puede parecer restrictivo, pero tiene beneficios prácticos:
- El código sigue siendo legible en pantallas pequeñas
- Puedes ver dos archivos lado a lado
- Fomenta dividir expresiones complejas en partes más simples
Nota: Muchos proyectos modernos usan límites ligeramente más largos (88, 100 o 120 caracteres). La clave es la consistencia dentro de tu proyecto. Elige un límite y respétalo.
# Demasiado largo: difícil de leer
# ADVERTENCIA: Estilo deficiente - solo para demostración
def calculate_monthly_payment(principal, annual_rate, years):
return principal * (annual_rate / 12) * (1 + annual_rate / 12) ** (years * 12) / ((1 + annual_rate / 12) ** (years * 12) - 1)
# Mejor: dividido en líneas legibles
def calculate_monthly_payment(principal, annual_rate, years):
"""Calculate monthly loan payment using amortization formula."""
monthly_rate = annual_rate / 12
num_payments = years * 12
numerator = principal * monthly_rate * (1 + monthly_rate) ** num_payments
denominator = (1 + monthly_rate) ** num_payments - 1
return numerator / denominator
payment = calculate_monthly_payment(200000, 0.045, 30)
print(f"Monthly payment: ${payment:.2f}") # Output: Monthly payment: $1013.37Dividir líneas largas: Cuando necesites dividir una línea, usa continuación implícita de línea dentro de paréntesis, corchetes o llaves:
# Llamada a función larga
result = some_function(
first_argument,
second_argument,
third_argument,
fourth_argument
)
# Lista larga
colors = [
"red", "green", "blue",
"yellow", "orange", "purple",
"pink", "brown", "gray"
]
# String largo
message = (
"This is a very long message that needs to be broken "
"across multiple lines for readability. Python automatically "
"concatenates adjacent string literals."
)
print(message)
# Output: This is a very long message that needs to be broken across multiple lines for readability. Python automatically concatenates adjacent string literals.40.3.3) Espaciado alrededor de operadores y después de comas
Usa espacios alrededor de operadores y después de comas para mejorar la legibilidad:
# Espaciado deficiente: apretado y difícil de leer
# ADVERTENCIA: Estilo deficiente - solo para demostración
x=5
y=x*2+3
result=calculate_tax(100,0.08)
data=[1,2,3,4,5]
# Buen espaciado: claro y legible
x = 5
y = x * 2 + 3
result = calculate_tax(100, 0.08)
data = [1, 2, 3, 4, 5]
# Espaciado en expresiones
total = (price * quantity) + shipping_cost
is_valid = (age >= 18) and (has_license == True)
# Espaciado en definiciones de funciones
def calculate_discount(price, discount_rate, minimum_purchase=0):
"""Calculate discounted price if minimum purchase is met."""
if price >= minimum_purchase:
return price * (1 - discount_rate)
return priceExcepción: No uses espacios alrededor de = en argumentos con palabra clave ni en valores por defecto de parámetros:
# Espaciado correcto para argumentos con palabra clave
result = calculate_discount(price=100, discount_rate=0.1, minimum_purchase=50)
# Espaciado correcto para parámetros por defecto
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"40.3.4) Líneas en blanco para separación lógica
Usa líneas en blanco para separar secciones lógicas de código:
Dos líneas en blanco entre funciones y clases de nivel superior:
def first_function():
"""First function."""
pass
def second_function():
"""Second function."""
pass
class MyClass:
"""A class definition."""
passUna línea en blanco entre métodos dentro de una clase:
class Student:
"""Represent a student with grades."""
def __init__(self, name):
self.name = name
self.grades = []
def add_grade(self, grade):
"""Add a grade to the student's record."""
self.grades.append(grade)
def get_average(self):
"""Calculate the student's grade average."""
if not self.grades:
return 0
return sum(self.grades) / len(self.grades)Líneas en blanco dentro de funciones para separar pasos lógicos:
def process_order(order_items, customer):
"""Process a customer order and calculate total."""
# Calcular subtotal
subtotal = 0
for item in order_items:
subtotal += item["price"] * item["quantity"]
# Aplicar descuento del cliente
discount = 0
if customer["is_premium"]:
discount = subtotal * 0.1
# Calcular impuesto
tax = (subtotal - discount) * 0.08
# Calcular total final
total = subtotal - discount + tax
return {
"subtotal": subtotal,
"discount": discount,
"tax": tax,
"total": total
}Estas líneas en blanco actúan como "párrafos" visuales, haciendo que la estructura del código sea evidente de inmediato.
40.3.5) Evitar espacios en blanco al final de la línea
No dejes espacios al final de las líneas—son invisibles pero pueden causar problemas con sistemas de control de versiones y algunos editores.
# Malo: espacios finales invisibles (mostrados como · solo para ilustración)
# ADVERTENCIA: Estilo deficiente - solo para demostración
def calculate(x):···
return x * 2···
# Bueno: sin espacios finales
def calculate(x):
return x * 2La mayoría de los editores modernos se pueden configurar para eliminar automáticamente los espacios en blanco al final cuando guardas un archivo.
40.4) Documentación: escribir comentarios y docstrings útiles
40.4.1) Cuándo escribir comentarios
Los comentarios explican por qué el código hace algo, no qué hace. Variables y funciones bien nombradas deberían hacer que el "qué" sea obvio.
# Comentario deficiente: dice lo obvio
# ADVERTENCIA: Estilo deficiente - solo para demostración
x = x + 1 # Add 1 to x
# Buen comentario: explica por qué
x = x + 1 # Ajustar por indexación basada en cero
# Comentario deficiente: redundante con el código
# ADVERTENCIA: Estilo deficiente - solo para demostración
# Comprobar si age es mayor o igual que 18
if age >= 18:
print("Adult")
# Buen comentario: explica la lógica de negocio
# Edad legal para beber en EE. UU.
if age >= 21:
print("Can purchase alcohol")Cuándo son valiosos los comentarios:
- Explicar algoritmos complejos:
def binary_search(sorted_list, target):
"""Search for target in sorted list using binary search."""
left = 0
right = len(sorted_list) - 1
while left <= right:
# Calcular el punto medio, evitando desbordamiento de enteros
# (right + left) // 2 podría desbordarse con índices muy grandes
mid = left + (right - left) // 2
if sorted_list[mid] == target:
return mid
elif sorted_list[mid] < target:
left = mid + 1 # El target está en la mitad derecha
else:
right = mid - 1 # El target está en la mitad izquierda
return -1 # No se encontró el target- Aclarar reglas de negocio no obvias:
def calculate_shipping_cost(weight, distance):
"""Calculate shipping cost based on weight and distance."""
base_cost = 5.00
# Promoción de envío gratis para artículos pesados (política de la empresa vigente desde 2024)
# Esto fomenta pedidos al por mayor y reduce los costes de envío por unidad
if weight > 50:
return 0
# Tarifa estándar: $0.50 por libra más $0.10 por milla
# Basado en el contrato del transportista negociado en el primer trimestre de 2024
return base_cost + (weight * 0.50) + (distance * 0.10)- Documentar soluciones alternativas o temporales:
def process_data(data):
"""Process incoming data records."""
# TODO: Este es un arreglo temporal para registros malformados
# Eliminar una vez que la validación de datos se implemente aguas arriba
if not isinstance(data, list):
data = [data]
for record in data:
# Procesar cada registro
pass40.4.2) Escribir docstrings efectivos
Las docstrings son comentarios especiales que documentan módulos, clases y funciones. Se encierran entre comillas triples y aparecen como la primera sentencia en la definición.
def calculate_bmi(weight_kg, height_m):
"""
Calculate Body Mass Index (BMI).
BMI is calculated as weight in kilograms divided by the square of height in meters.
Args:
weight_kg: Weight in kilograms (float or int)
height_m: Height in meters (float or int)
Returns:
float: The calculated BMI value
Example:
>>> calculate_bmi(70, 1.75)
22.857142857142858
"""
return weight_kg / (height_m ** 2)
# Acceder a docstrings
print(calculate_bmi.__doc__)
# Output:
# Calculate Body Mass Index (BMI).
#
# BMI is calculated as weight in kilograms divided by the square of height in meters.
# ...Docstrings de una línea para funciones simples:
def square(x):
"""Return the square of x."""
return x * x
def is_even(n):
"""Return True if n is even, False otherwise."""
return n % 2 == 0Docstrings de varias líneas para funciones complejas:
def find_prime_factors(n):
"""
Find all prime factors of a positive integer.
This function returns a list of prime numbers that, when multiplied
together, equal the input number. The factors are returned in ascending order.
Args:
n: A positive integer greater than 1
Returns:
list: Prime factors in ascending order
Raises:
ValueError: If n is less than 2
Example:
>>> find_prime_factors(12)
[2, 2, 3]
>>> find_prime_factors(17)
[17]
"""
if n < 2:
raise ValueError("n must be at least 2")
factors = []
divisor = 2
while n > 1:
while n % divisor == 0:
factors.append(divisor)
n = n // divisor
divisor += 1
return factorsDocstrings de clase:
class BankAccount:
"""
Represent a bank account with deposit and withdrawal operations.
This class maintains an account balance and provides methods for
depositing and withdrawing money. All transactions are validated to prevent negative balances.
Attributes:
account_number: Unique identifier for the account
balance: Current account balance in dollars
"""
def __init__(self, account_number, initial_balance=0):
"""
Initialize a new bank account.
Args:
account_number: Unique account identifier (string)
initial_balance: Starting balance (default: 0)
"""
self.account_number = account_number
self.balance = initial_balance
def deposit(self, amount):
"""
Add money to the account.
Args:
amount: Amount to deposit (must be positive)
Raises:
ValueError: If amount is not positive
"""
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.balance += amount40.4.3) Convenciones de docstrings
Primera línea: Resumen breve de lo que hace la función/clase. Debe caber en una línea.
Línea en blanco: Separa el resumen de la descripción detallada.
Descripción detallada: Explica lo que hace la función, cualquier detalle importante y cómo usarla.
Args/Parameters: Enumera cada parámetro con su tipo y propósito.
Returns: Describe qué devuelve la función y su tipo.
Raises: Documenta cualquier excepción que la función pueda lanzar.
Example: Muestra un uso típico (opcional pero útil).
def calculate_compound_interest(principal, rate, time, compounds_per_year=1):
"""
Calculate compound interest on an investment.
Uses the compound interest formula: A = P(1 + r/n)^(nt)
where A is the final amount, P is principal, r is annual rate,
n is compounds per year, and t is time in years.
Args:
principal: Initial investment amount (float)
rate: Annual interest rate as decimal (e.g., 0.05 for 5%)
time: Investment period in years (float)
compounds_per_year: Number of times interest compounds annually
(default: 1 for annual compounding)
Returns:
float: Final amount after compound interest
Example:
>>> calculate_compound_interest(1000, 0.05, 10, 12)
1647.0095
"""
return principal * (1 + rate / compounds_per_year) ** (compounds_per_year * time)40.4.4) Comentarios TODO para trabajo futuro
Usa comentarios TODO para marcar áreas que requieren atención en el futuro:
def process_payment(amount, payment_method):
"""Process a payment transaction."""
# TODO: Añadir soporte para pagos con criptomonedas
# TODO: Implementar comprobaciones de detección de fraude
if payment_method == "credit_card":
return process_credit_card(amount)
elif payment_method == "paypal":
return process_paypal(amount)
else:
raise ValueError(f"Unsupported payment method: {payment_method}")Muchos editores pueden buscar comentarios TODO, lo que facilita encontrar áreas que requieren trabajo.
40.5) Organizar tu código: importaciones, constantes, funciones y main
40.5.1) Estructura estándar de un módulo
Un módulo Python bien organizado sigue esta estructura:
- Docstring del módulo: Describe qué hace el módulo
- Importaciones: Biblioteca estándar, terceros y luego importaciones locales
- Constantes: Constantes a nivel de módulo
- Funciones y clases: Código principal
- Bloque de ejecución principal: Código que se ejecuta cuando se ejecuta el script
"""
student_manager.py
Manage student records including grades and GPA calculations.
This module provides functions for adding students, recording grades,
and calculating grade point averages.
"""
# Importaciones de la biblioteca estándar
import sys
from datetime import datetime
# Importaciones de terceros (si las hay)
# import requests
# Importaciones locales (si las hay)
# from .database import save_student
# Constantes
MAX_GRADE = 100
MIN_GRADE = 0
PASSING_GRADE = 60
# Funciones
def calculate_gpa(grades):
"""
Calculate GPA from a list of numeric grades.
Args:
grades: List of numeric grades (0-100)
Returns:
float: GPA on 4.0 scale
"""
if not grades:
return 0.0
average = sum(grades) / len(grades)
# Convertir a escala 4.0
if average >= 90:
return 4.0
elif average >= 80:
return 3.0
elif average >= 70:
return 2.0
elif average >= 60:
return 1.0
else:
return 0.0
def validate_grade(grade):
"""
Check if a grade is within valid range.
Args:
grade: Numeric grade to validate
Returns:
bool: True if grade is valid, False otherwise
"""
return MIN_GRADE <= grade <= MAX_GRADE
# Ejecución principal
if __name__ == "__main__":
# Código que se ejecuta cuando el script se ejecuta directamente
test_grades = [85, 92, 78, 88]
gpa = calculate_gpa(test_grades)
print(f"GPA: {gpa}") # Output: GPA: 3.040.5.2) Organización de importaciones
Agrupa las importaciones en tres secciones, separadas por líneas en blanco:
- Importaciones de la biblioteca estándar: Módulos integrados de Python
- Importaciones de terceros: Paquetes instalados (como
requests,numpy) - Importaciones locales: Tus propios módulos
# Importaciones de la biblioteca estándar
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
# Importaciones de terceros
import requests
from flask import Flask, render_template
# Importaciones locales de la aplicación
from myapp.database import connect_db
from myapp.models import User, Product
from myapp.utils import format_currencyEstilos de importación:
# Importar el módulo completo
import math
result = math.sqrt(16) # Output: 4.0
# Importar elementos específicos
from math import sqrt, pi
result = sqrt(16) # Output: 4.0
# Importar con alias
import numpy as np
array = np.array([1, 2, 3])
# Importar múltiples elementos
from os import path, getcwd, listdirEvita importaciones comodín (from module import *)—hacen que no esté claro de dónde vienen los nombres:
# Malo: no queda claro de dónde viene sqrt
# ADVERTENCIA: Estilo deficiente - solo para demostración
from math import *
result = sqrt(16)
# Bueno: importación explícita
from math import sqrt
result = sqrt(16)40.5.3) Organizar constantes
Coloca las constantes a nivel de módulo cerca de la parte superior, después de las importaciones:
"""Configuration settings for the application."""
import os
# Constantes de la aplicación
APP_NAME = "Student Manager"
VERSION = "1.0.0"
DEBUG_MODE = True
# Configuración de base de datos
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///students.db")
MAX_CONNECTIONS = 10
# Reglas de negocio
MAX_STUDENTS_PER_CLASS = 30
PASSING_GRADE = 60
GRADE_WEIGHTS = {
"homework": 0.3,
"midterm": 0.3,
"final": 0.4
}
def calculate_final_grade(homework, midterm, final):
"""Calculate weighted final grade."""
return (
homework * GRADE_WEIGHTS["homework"] +
midterm * GRADE_WEIGHTS["midterm"] +
final * GRADE_WEIGHTS["final"]
)40.5.4) Orden lógico de funciones
Organiza las funciones en un orden lógico:
- Primero las funciones públicas: Funciones pensadas para ser usadas por otros módulos
- Después las funciones auxiliares: Funciones internas que apoyan a las públicas
- Funciones relacionadas juntas: Agrupa funciones que trabajan en conjunto
"""Order processing module."""
# Funciones API públicas
def process_order(order_items, customer):
"""
Process a customer order.
This is the main entry point for order processing.
"""
subtotal = _calculate_subtotal(order_items)
discount = _calculate_discount(subtotal, customer)
tax = _calculate_tax(subtotal - discount)
total = subtotal - discount + tax
return {
"subtotal": subtotal,
"discount": discount,
"tax": tax,
"total": total
}
def validate_order(order_items):
"""Validate that an order contains valid items."""
if not order_items:
return False
for item in order_items:
if not _validate_item(item):
return False
return True
# Funciones auxiliares internas
def _calculate_subtotal(items):
"""Calculate order subtotal (internal use)."""
total = 0
for item in items:
total += item["price"] * item["quantity"]
return total
def _calculate_discount(subtotal, customer):
"""Calculate customer discount (internal use)."""
if customer.get("is_premium"):
return subtotal * 0.1
return 0
def _calculate_tax(amount):
"""Calculate sales tax (internal use)."""
TAX_RATE = 0.08
return amount * TAX_RATE
def _validate_item(item):
"""Validate a single order item (internal use)."""
required_fields = ["name", "price", "quantity"]
return all(field in item for field in required_fields)Observa cómo las funciones públicas (process_order, validate_order) van primero, y las funciones auxiliares (con prefijo _) van después. Esto deja claro cuáles son las funciones de la API principal.
40.5.5) Organización de clases dentro de módulos
Cuando un módulo contiene clases, organízalas de forma lógica:
"""User management system."""
# Constantes
DEFAULT_ROLE = "user"
ADMIN_ROLE = "admin"
# Primero las clases base
class User:
"""Base user class."""
def __init__(self, username, email):
self.username = username
self.email = email
self.role = DEFAULT_ROLE
def can_edit(self, resource):
"""Check if user can edit a resource."""
return resource.owner == self.username
# Después las clases derivadas
class AdminUser(User):
"""Administrator with elevated privileges."""
def __init__(self, username, email):
super().__init__(username, email)
self.role = ADMIN_ROLE
def can_edit(self, resource):
"""Admins can edit any resource."""
return True
# Clases relacionadas agrupadas
class Resource:
"""Represent a resource that can be owned and edited."""
def __init__(self, name, owner):
self.name = name
self.owner = owner
# Funciones de utilidad relacionadas con las clases
def create_user(username, email, is_admin=False):
"""Factory function to create appropriate user type."""
if is_admin:
return AdminUser(username, email)
return User(username, email)Principios de organización de clases:
- Clases base antes que clases derivadas (los lectores necesitan entender la base primero)
- Clases relacionadas agrupadas (User y Resource están relacionadas)
- Las funciones de utilidad que trabajan con clases van después de las definiciones de clase
- Cada clase debe tener una docstring clara que explique su propósito
40.6) El patrón if name == "main"
40.6.1) Comprender el patrón
Cada archivo Python tiene una variable integrada llamada __name__. Python establece automáticamente el valor de esta variable dependiendo de cómo se esté usando el archivo:
- Cuando ejecutas un archivo directamente (p. ej.,
python my_script.py), Python establece__name__en"__main__" - Cuando importas un archivo como módulo, Python establece
__name__en el nombre del módulo (el nombre del archivo sin.py)
Esto te permite escribir código que solo se ejecuta cuando ejecutas el archivo directamente, no cuando se importa:
"""math_utils.py - Mathematical utility functions."""
def add(a, b):
"""Add two numbers."""
return a + b
def multiply(a, b):
"""Multiply two numbers."""
return a * b
# Este código solo se ejecuta cuando el archivo se ejecuta directamente
if __name__ == "__main__":
# Probar las funciones
print(f"5 + 3 = {add(5, 3)}") # Output: 5 + 3 = 8
print(f"5 * 3 = {multiply(5, 3)}") # Output: 5 * 3 = 15Cuando ejecutas python math_utils.py, verás la salida. Pero cuando lo importas en otro archivo:
# another_file.py
from math_utils import add, multiply
result = add(10, 20)
print(result) # Output: 30
# El código de prueba de math_utils.py NO se ejecuta¡Fíjate que el código de prueba (dentro de if __name__ == "__main__":) NO se ejecuta cuando se importa!
40.6.2) Por qué importa este patrón
Este patrón cumple varios propósitos importantes:
1. Pruebas y demostración: puedes incluir ejemplos de uso en el mismo archivo que tus funciones:
"""temperature.py - Temperature conversion utilities."""
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit."""
return (celsius * 9/5) + 32
def fahrenheit_to_celsius(fahrenheit):
"""Convert Fahrenheit to Celsius."""
return (fahrenheit - 32) * 5/9
if __name__ == "__main__":
# Demostrar las funciones
print("Temperature Conversion Examples:")
print(f"0°C = {celsius_to_fahrenheit(0)}°F") # Output: 0°C = 32.0°F
print(f"100°C = {celsius_to_fahrenheit(100)}°F") # Output: 100°C = 212.0°F
print(f"32°F = {fahrenheit_to_celsius(32)}°C") # Output: 32°F = 0.0°C2. Módulos reutilizables: el mismo archivo puede ser tanto un script autónomo como un módulo importable:
"""data_processor.py - Process and analyze data files."""
import sys
def load_data(filename):
"""Load data from a file."""
with open(filename) as f:
return [line.strip() for line in f]
def analyze_data(data):
"""Perform analysis on data."""
return {
"count": len(data),
"average_length": sum(len(item) for item in data) / len(data)
}
if __name__ == "__main__":
# Cuando se ejecuta como script, procesar argumentos de línea de comandos
if len(sys.argv) < 2:
print("Usage: python data_processor.py <filename>")
sys.exit(1)
filename = sys.argv[1]
data = load_data(filename)
results = analyze_data(data)
print(f"Processed {results['count']} items")
print(f"Average length: {results['average_length']:.2f}")Puedes ejecutarlo como script:
$ python data_processor.py data.txt
Processed 42 items
Average length: 15.23O importarlo en otro archivo:
# my_analysis.py
from data_processor import load_data, analyze_data
my_data = load_data("myfile.txt")
results = analyze_data(my_data)
print(f"Found {results['count']} items")40.6.3) Patrones comunes para bloques main
Patrón 1: casos de prueba simples
"""calculator.py - Basic calculator operations."""
def add(a, b):
"""Add two numbers."""
return a + b
def subtract(a, b):
"""Subtract b from a."""
return a - b
if __name__ == "__main__":
# Pruebas rápidas
assert add(2, 3) == 5
assert subtract(10, 4) == 6
print("All tests passed!") # Output: All tests passed!Patrón 2: función main
Para scripts más complejos, define una función main():
"""report_generator.py - Generate reports from data."""
import sys
def load_data(filename):
"""Load data from file."""
# Implementation here
pass
def generate_report(data):
"""Generate report from data."""
# Implementation here
pass
def save_report(report, output_file):
"""Save report to file."""
# Implementation here
pass
def main():
"""Main entry point for the script."""
if len(sys.argv) < 3:
print("Usage: python report_generator.py <input> <output>")
return 1
input_file = sys.argv[1]
output_file = sys.argv[2]
try:
data = load_data(input_file)
report = generate_report(data)
save_report(report, output_file)
print(f"Report saved to {output_file}")
return 0
except Exception as e:
print(f"Error: {e}")
return 1
if __name__ == "__main__":
# Salir con el código de estado de main (0 = éxito, 1 = error)
sys.exit(main())Este patrón tiene varias ventajas:
- La función
main()se puede probar de forma independiente - Punto de entrada claro para el script
- Códigos de salida adecuados (0 para éxito, distinto de cero para errores)
- Separación limpia entre la lógica del script y las funciones del módulo
40.6.4) Buenas prácticas para bloques main
Mantén los bloques main enfocados: el código dentro de if __name__ == "__main__" debería manejar principalmente la ejecución del script, no contener lógica compleja:
# Malo: lógica compleja en el bloque main
# ADVERTENCIA: Estilo deficiente - solo para demostración
if __name__ == "__main__":
data = []
for i in range(100):
if i % 2 == 0:
data.append(i * 2)
result = sum(data) / len(data)
print(result)
# Bueno: la lógica en funciones, el bloque main coordina
def generate_even_doubles(limit):
"""Generate doubled even numbers up to limit."""
return [i * 2 for i in range(limit) if i % 2 == 0]
def calculate_average(numbers):
"""Calculate average of numbers."""
return sum(numbers) / len(numbers)
if __name__ == "__main__":
data = generate_even_doubles(100)
result = calculate_average(data)
print(result) # Output: 99.0Usa una función main() para scripts complejos: como se mostró antes, definir una función main() hace que tu script sea más testeable y organizado.
Documenta el uso del script: si tu script acepta argumentos de línea de comandos, documéntalos en la docstring del módulo:
"""
file_processor.py - Process text files with various operations.
Usage:
python file_processor.py <input_file> <output_file> [--uppercase]
Arguments:
input_file: Path to input file
output_file: Path to output file
--uppercase: Convert text to uppercase (optional)
"""
import sys
def process_file(input_path, output_path, uppercase=False):
"""Process file with specified options."""
with open(input_path) as f:
content = f.read()
if uppercase:
content = content.upper()
with open(output_path, 'w') as f:
f.write(content)
if __name__ == "__main__":
if len(sys.argv) < 3:
print(__doc__) # Imprimir la docstring del módulo
sys.exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2]
uppercase = "--uppercase" in sys.argv
process_file(input_file, output_file, uppercase)
print(f"Processed {input_file} -> {output_file}")Escribir código limpio y legible es una habilidad que se desarrolla con la práctica. Las convenciones y patrones de este capítulo no son reglas arbitrarias—son prácticas probadas que hacen que el código sea más fácil de entender, mantener y depurar. A medida que escribas más código Python, estos patrones se volverán algo natural.
Recuerda: el código se lee mucho más a menudo de lo que se escribe. Los pocos segundos extra que dedicas a elegir un nombre claro, añadir un comentario útil u organizar tus importaciones correctamente te ahorrarán horas de confusión más tarde—para ti y para otros que trabajen con tu código.
En el próximo capítulo, exploraremos técnicas de depuración y pruebas que se basan en estas prácticas de código limpio, ayudándote a escribir no solo código legible, sino código correcto y fiable.