Cola de Prioridad en Python
Introducción
Una cola de prioridad (priority queue) es una abstracción que permite almacenar elementos asociados a una prioridad. Cuando se extrae un elemento, siempre se devuelve aquel con la prioridad más alta (o más baja, según la convención). Esta estructura es fundamental en algoritmos como Dijkstra, A* y en sistemas de gestión de tareas.
En Python existen varias formas de implementar una cola de prioridad: el módulo estándar heapq, la clase queue.PriorityQueue (thread‑safe) y soluciones personalizadas basadas en árboles binarios o Fibonacci heaps.
¿Cómo funciona internamente?
La implementación más común es el montículo binario (binary heap), una estructura de árbol casi completo representada en un list. Sus operaciones principales tienen complejidad:
- push (insertar):
O(log n) - pop (extraer máximo/mínimo):
O(log n) - peek (mirar el elemento de mayor prioridad):
O(1)
El heap mantiene la propiedad de que cada nodo es menor (o mayor) que sus hijos, garantizando que el elemento de mayor prioridad siempre está en la raíz (heap[0]).
Implementación con heapq
El módulo heapq provee funciones de bajo nivel para trabajar con listas como montículos min‑heap.
import heapq
# Cada elemento es una tupla (prioridad, dato)
cola = []
# Insertar elementos
heapq.heappush(cola, (2, "tarea media"))
heapq.heappush(cola, (1, "tarea alta"))
heapq.heappush(cola, (5, "tarea baja"))
# Obtener el elemento de mayor prioridad (menor número)
prioridad, tarea = heapq.heappop(cola)
print(prioridad, tarea) # → 1 tarea alta
Si necesitas que la prioridad sea “más alta = número mayor”, basta invertir el valor o usar una capa de envoltura (NegatedPriority).
heapq.heappush(cola, (-prioridad, dato)) # max‑heap simulada
heapq no es thread‑safe. Si varios hilos acceden a la cola, envuélvela con threading.Lock o usa queue.PriorityQueue.
Clase wrapper: PriorityQueue de queue
Para entornos multihilo, queue.PriorityQueue brinda sincronización interna y bloquea automáticamente cuando la cola está vacía o llena.
from queue import PriorityQueue
pq = PriorityQueue()
# Encolar (prioridad, elemento)
for i in range(5):
pq.put((i, f"tarea-{i}"))
while not pq.empty():
prioridad, tarea = pq.get()
print(prioridad, tarea)
Internamente utiliza heapq y un Lock de threading, por lo que su rendimiento es ligeramente inferior al de heapq puro, pero garantiza seguridad en concurrencia.
Implementación personalizada con dataclass y soporte de comparación
Cuando los elementos son objetos complejos, es útil definir una dataclass que implemente los operadores de comparación.
from __future__ import annotations
from dataclasses import dataclass, field
import heapq
@dataclass(order=True)
class Tarea:
prioridad: int
nombre: str = field(compare=False)
payload: dict = field(default_factory=dict, compare=False)
cola: list[Tarea] = []
heapq.heappush(cola, Tarea(3, "Generar reporte"))
heapq.heappush(cola, Tarea(1, "Enviar alerta"))
heapq.heappush(cola, Tarea(2, "Sincronizar DB"))
while cola:
tarea = heapq.heappop(cola)
print(f"[{tarea.prioridad}] {tarea.nombre}")
Al marcar order=True, la dataclass genera automáticamente los métodos __lt__, __gt__, etc., basándose en los campos marcados como comparables.
Comparativa con otras tecnologías
| Implementación | Ventajas | Desventajas | Uso típico |
|---|---|---|---|
heapq |
Ligero, O(log n) garantizado, sin dependencias |
No thread‑safe, solo min‑heap (requiere inversión para max‑heap) | Algoritmos de grafos, juegos, simulaciones |
queue.PriorityQueue |
Seguridad de hilos, bloqueo automático | Mayor overhead, menos control directo sobre el heap interno | Servidores web, workers de cola de mensajes |
Biblioteca sortedcontainers.SortedList |
Acceso aleatorio O(log n), permite búsquedas por rango |
Mayor consumo de memoria, inserción algo más costosa | Planificadores con prioridad variable y consultas de rango |
Redis ZSET |
Persistencia, distribución, operaciones atómicas | Requiere servidor externo, latencia de red | Colas de trabajos a gran escala, throttling de API |
Rendimiento y escalabilidad
Para medir el rendimiento de una cola de prioridad en Python, se pueden usar los módulos timeit o perf_counter:
import heapq, random, time
N = 1_000_000
items = [(random.randint(1, 1_000_000), i) for i in range(N)]
start = time.perf_counter()
heap = []
for it in items:
heapq.heappush(heap, it)
push_time = time.perf_counter() - start
start = time.perf_counter()
while heap:
heapq.heappop(heap)
pop_time = time.perf_counter() - start
print(f"push: {push_time:.3f}s, pop: {pop_time:.3f}s")
En máquinas modernas, heapq gestiona ~1M de elementos en menos de 2 s (push + pop). Si la carga supera la memoria RAM, considera una cola basada en disco (por ejemplo, sqlite con índice) o una solución distribuida como Redis.
Buenas prácticas y consideraciones de seguridad
- Normaliza prioridades: usa siempre el mismo rango (ej. 0‑100) para evitar overflow al invertir valores.
- Evita mutar objetos después de insertarlos: si cambias la prioridad de un elemento ya en el heap, el orden quedará corrupto. En su lugar, elimina y vuelve a insertar.
- Control de acceso: si la cola contiene datos sensibles, protege el acceso con mecanismos de autorización (por ejemplo, wrappers que verifiquen tokens antes de
get). - Persistencia opcional: para procesos críticos, guarda el estado del heap en disco (pickle, json) antes de cerrar la aplicación y recárgalo al iniciar.
- Monitoreo de latencia: registra tiempos de espera en colas de alta concurrencia; una cola saturada suele indicar problemas de dimensionamiento de workers.
Resolución de problemas (troubleshooting)
Síntoma: "pop() devuelve un elemento con prioridad incorrecta"
Causas comunes:
- Se modificó la prioridad del objeto después de insertarlo.
- Se utilizó una tupla donde el primer elemento no representa la prioridad (orden invertido).
- Se mezclaron tipos incompatibles (int vs. str) en la posición de prioridad.
Solución: siempre inserta una copia inmutable (por ejemplo, una tupla) y evita mutaciones posteriores.
Síntoma: "La aplicación se bloquea al usar PriorityQueue en varios hilos"
Causa: deadlock por uso de get() sin timeout mientras otro hilo mantiene el lock.
Solución: usa get(timeout=5) o queue.Empty para romper el ciclo y revisar los logs.
Casos de uso reales
- Planificador de tareas en microservicios: cada petición recibe una prioridad basada en SLA; los workers consumen de una
PriorityQueuepara garantizar que los clientes premium sean atendidos primero. - Algoritmo de Dijkstra: el heap mantiene los nodos aún no procesados con la distancia mínima encontrada.
- Motor de juego (IA): eventos de IA y físicas se ordenan por tiempo de ejecución o urgencia.
- Rate limiting distribuido: usando Redis
ZSETcomo cola de prioridad para controlar la tasa de peticiones a APIs externas.
Conclusión
Las colas de prioridad son una herramienta esencial en la caja de herramientas de cualquier ingeniero de software. Python ofrece varias opciones, desde el ligero heapq hasta la versión thread‑safe queue.PriorityQueue, y es posible crear clases personalizadas con dataclass para adaptarse a dominios complejos. Al seguir las mejores prácticas de normalización, inmovilidad de datos y monitoreo, podrás escalar tus sistemas de forma segura y eficiente.
Cola de Prioridad en Python: Conceptos, Implementaciones y Mejores Prácticas