Errores Comunes a Evitar
Prácticas de programación que degradan la calidad de tu código Python
Los anti-patterns son prácticas de programación comunes que, aunque no necesariamente incorrectas, frecuentemente conducen a código menos eficiente, menos legible y menos mantenible. Al comprender estas trampas, puedes escribir código más limpio y profesional.
En este artículo exploraremos cuatro categorías de anti-patterns: Violaciones de Estilo, Anti-patterns de Correctitud, Anti-patterns de Mantenibilidad y Anti-patterns de Rendimiento.
Violaciones de Estilo de Código
La guía de estilo de Python, conocida como PEP 8, proporciona recomendaciones para mejorar la legibilidad y consistencia del código. Seguir estas convenciones facilita la colaboración entre desarrolladores y el mantenimiento a largo plazo.
Herramientas de Formateo
Indentación
Debes usar cuatro espacios por nivel de indentación y evitar mezclar tabs con espacios.
def my_function(): # 2 espacios - inconsistente if True: # 4 espacios - mezclado print("Bad")
def my_function(): # 4 espacios consistentes if True: # 4 espacios más print("Good")
Longitud de Línea y Líneas en Blanco
La guía recomienda limitar todas las líneas de código a un máximo de 79 caracteres para mejor legibilidad. También existen reglas sobre líneas en blanco:
Imports
Los imports deben organizarse en grupos separados por líneas en blanco, en este orden:
# 1. Imports de biblioteca estándar import os import sys # 2. Imports de terceros import requests import numpy as np # 3. Imports locales/aplicación from myproject import utils from myproject.models import User
import os, sys es incorrecto. Cada import debe estar en su propia línea.
Convenciones de Nombrado
# Clases: CapWords (PascalCase) class MyClassName: pass # Funciones y variables: snake_case def my_function_name(): my_variable = 42 return my_variable # Constantes: SCREAMING_SNAKE_CASE MAX_CONNECTIONS = 100 DEFAULT_TIMEOUT = 30 # Atributos protegidos: _prefijo_underscore class Example: def __init__(self): self._protected_attr = "internal"
Espacios en Expresiones
# Espacios dentro de paréntesis spam( ham[ 1 ], { eggs: 2 } ) # Espacio antes de paréntesis spam (1) # Múltiples espacios alrededor de = x = 1 long_variable = 2
# Sin espacios dentro de paréntesis spam(ham[1], {eggs: 2}) # Sin espacio antes de paréntesis spam(1) # Un espacio alrededor de = x = 1 long_variable = 2
Anti-patterns de Correctitud
Estos anti-patterns pueden llevar a bugs sutiles y comportamientos inesperados en tu código. Son especialmente peligrosos porque el código puede parecer funcionar correctamente en casos simples.
Usar type() para Comparar Tipos
Usar type() para comparar tipos ignora la herencia y el polimorfismo, lo cual es fundamental en Python.
def process(obj): # No reconoce subclases if type(obj) == dict: return "Es un dict" return "No es un dict" # defaultdict ES un dict, pero... from collections import defaultdict d = defaultdict(int) print(process(d)) # "No es un dict" ¡ERROR!
def process(obj): # isinstance() respeta herencia if isinstance(obj, dict): return "Es un dict" return "No es un dict" from collections import defaultdict d = defaultdict(int) print(process(d)) # "Es un dict" ✓
Argumento Mutable por Defecto
Este es uno de los bugs más comunes en Python. Los argumentos mutables por defecto se evalúan una sola vez cuando se define la función, no cada vez que se llama.
def add_item(item, items=[]): # ⚠️ PELIGRO items.append(item) return items # Primera llamada print(add_item("a")) # ['a'] - Parece correcto # Segunda llamada - ¡SORPRESA! print(add_item("b")) # ['a', 'b'] - ¡La lista persiste! # Tercera llamada print(add_item("c")) # ['a', 'b', 'c'] - Sigue acumulando
def add_item(item, items=None): # ✓ Correcto if items is None: items = [] # Nueva lista cada vez items.append(item) return items print(add_item("a")) # ['a'] print(add_item("b")) # ['b'] - Lista fresca print(add_item("c")) # ['c'] - Lista fresca
Acceder a Miembros Protegidos desde Fuera
Acceder a atributos con prefijo underscore desde fuera de la clase rompe la encapsulación y hace el código frágil ante cambios internos.
class Book: def __init__(self, title, author): self._title = title # Protegido self._author = author # Protegido @property def title(self): return self._title @property def author(self): return self._author book = Book("1984", "Orwell") # ✗ No hacer esto # print(book._title) # Acceso directo a protegido # ✓ Usar la propiedad pública print(book.title) # "1984" print(book.author) # "Orwell"
Anti-patterns de Mantenibilidad
Estos anti-patterns hacen que tu código sea difícil de entender y mantener a lo largo del tiempo. Evitarlos mejora significativamente la calidad de tu base de código.
Imports con Wildcard
Usar from module import * contamina el namespace y hace difícil determinar de dónde viene cada función o variable.
from math import * from statistics import * # ¿De dónde viene sqrt? # ¿Hay conflictos de nombres? result = sqrt(16)
from math import sqrt, pi from statistics import mean # Claro de dónde viene cada cosa result = sqrt(16) average = mean([1, 2, 3])
LBYL vs EAFP
LBYL (Look Before You Leap) verifica condiciones antes de actuar. EAFP (Easier to Ask Forgiveness than Permission) intenta la acción y maneja excepciones. Python favorece EAFP.
import os # LBYL - Look Before You Leap def read_file_lbyl(filename): if os.path.exists(filename): with open(filename) as f: return f.read() else: print("Archivo no encontrado") return None
# EAFP - Easier to Ask Forgiveness than Permission # Más Pythonic y evita condiciones de carrera def read_file_eafp(filename): try: with open(filename) as f: return f.read() except FileNotFoundError: print("Archivo no encontrado") return None
Sobreuso de Herencia
Crear jerarquías de herencia profundas aumenta la complejidad y el acoplamiento. Favorece la composición sobre la herencia.
class GrandParent: pass class Parent(GrandParent): pass class Child(Parent): pass # Alto acoplamiento # Difícil de modificar
class Parent: pass class Child: def __init__(self, parent): self.parent = parent # Bajo acoplamiento # Flexible y testeable
Variables Globales para Compartir Datos
Las variables globales son accesibles en todo el programa, pero llevan a bugs donde diferentes partes modifican el estado inesperadamente. También causan problemas en entornos multithreading.
# ✗ Variable global - problemático counter = 0 def increment(): global counter counter += 1 def reset(): global counter counter = 0 # Cualquier parte del código puede modificar counter # Difícil de rastrear cambios # Problemas en multithreading
# ✓ Estado encapsulado en clase class Counter: def __init__(self): self.value = 0 def increment(self): self.value += 1 def reset(self): self.value = 0 # Uso c = Counter() c.increment() print(c.value) # 1 c.reset() print(c.value) # 0
0
Anti-patterns de Rendimiento
Estos anti-patterns llevan a ineficiencias que pueden degradar el rendimiento, especialmente notables en aplicaciones de gran escala o tareas intensivas en datos.
Concatenar Strings en un Loop
Concatenar strings con + o += en un loop crea un nuevo objeto string cada vez, lo cual es altamente ineficiente. Cada concatenación tiene complejidad O(n).
# ✗ Ineficiente - O(n²) def concatenate_bad(items): result = "" for item in items: result += item # Crea nuevo string cada vez return result # ✓ Eficiente - O(n) def concatenate_good(items): return "".join(items) # Una sola operación # Test con lista grande words = ["word"] * 10000 import timeit bad_time = timeit.timeit(lambda: concatenate_bad(words), number=100) good_time = timeit.timeit(lambda: concatenate_good(words), number=100) print(f"Concatenación +=: {bad_time:.4f}s") print(f"Usando .join(): {good_time:.4f}s")
Usando .join(): 0.0156s
Variables Globales para Caching
Usar variables globales para almacenar caché es problemático: no es thread-safe, no tiene control de tamaño, y contamina el namespace global.
import random import time # ✗ Caché global - problemático _cache = {} def get_data(user_id): if user_id not in _cache: # Simular operación costosa time.sleep(random.uniform(0.5, 1.0)) _cache[user_id] = {"name": f"User_{user_id}"} return _cache[user_id] # Problemas: # - No es thread-safe # - Sin límite de tamaño # - Sin política de expiración
import random import time from functools import lru_cache @lru_cache(maxsize=100) def get_data(user_id): return _perform_expensive_operation(user_id) def _perform_expensive_operation(user_id): time.sleep(random.uniform(0.5, 1.0)) user_data = { 1: {"name": "Alice", "email": "alice@example.com"}, 2: {"name": "Bob", "email": "bob@example.com"}, 3: {"name": "Charlie", "email": "charlie@example.com"}, } return user_data.get(user_id, {"error": "User not found"}) # Uso print(get_data(1)) # Lento - primera vez print(get_data(1)) # Rápido - desde caché print(get_data(2)) # Lento - nuevo usuario print(get_data(99)) # Lento - usuario no existe
{'name': 'Alice', 'email': 'alice@example.com'}
{'name': 'Bob', 'email': 'bob@example.com'}
{'error': 'User not found'}
@lru_cache proporciona un caché LRU (Least Recently Used) que automáticamente gestiona el tamaño y la vida útil de las entradas del caché.
Resumen de Anti-Patterns
01 // Estilo de Código
Sigue PEP 8: indentación consistente, imports organizados, nombres descriptivos. Usa herramientas como Black y Ruff.
02 // Correctitud
Usa isinstance() sobre type(), evita argumentos mutables por defecto, respeta la encapsulación.
03 // Mantenibilidad
Evita wildcard imports, favorece EAFP sobre LBYL, usa composición sobre herencia profunda.
04 // Rendimiento
Usa .join() para concatenar strings, emplea @lru_cache en lugar de variables globales para caching.