Guía Completa de la Estructura de Pila (Stack) en Python
En este artículo exploraremos en profundidad la pila (stack), una de las estructuras de datos más simples pero poderosas. Veremos su teoría, implementaciones idiomáticas en Python, casos de uso del mundo real, comparativas con otras estructuras y recomendaciones de rendimiento, seguridad, compatibilidad y despliegue moderno.
- Benchmarks reproducibles y perfiles de memoria con
tracemalloc. - Patrones avanzados: pila monotónica, MinStack O(1), pila persistente, integración asíncrona.
- Concurrencia:
threading,multiprocessingyqueue.LifoQueue. - Compatibilidad por versión/implementación de Python (CPython, PyPy).
- Buenas prácticas de seguridad, límites de capacidad y auditoría.
- Contenedores: ejecución y pruebas con Docker y Podman.
1. ¿Qué es una pila?
Una pila es una colección ordenada que sigue la política LIFO (Last‑In, First‑Out). El último elemento insertado es el primero en ser removido. Sus operaciones básicas son:
push(item): inserta un elemento en la cima.pop(): elimina y devuelve el elemento de la cima.peek()otop(): devuelve el elemento de la cima sin eliminarlo.is_empty(): indica si la pila está vacía.size(): número de elementos.
Esta simplicidad la convierte en la base de algoritmos de recorrido, gestión de llamadas recursivas y evaluación de expresiones.
1.1. Propiedades clave
- Determinista: el orden de extracción está completamente determinado por el orden de inserción.
- Complejidad:
push/popO(1) amortizada en implementaciones típicas. - Localidad de referencia: alta para operaciones en el extremo derecho en
listydeque. - Memoria: depende de la colección subyacente (estrategias de sobrerreserva en
listvs bloques endeque).
1.2. Cuándo NO usar una pila
- Si necesitas acceso aleatorio a índices intermedios: usa
listoarray. - Si requieres ordenamiento dinámico por prioridad: usa
heapq(cola de prioridad). - Si necesitas inserciones/eliminaciones frecuentes en el medio: considera
bisecto estructuras enlazadas (con librerías específicas).
2. Implementaciones idiomáticas en Python
Python no tiene una clase Stack dedicada en la biblioteca estándar, pero existen varias formas de modelarla eficazmente.
2.1. Usando list (más común)
Las listas proporcionan append() y pop() amortizados O(1) en el extremo derecho, por lo que funcionan como una pila.
# pila_lista.py
stack = [] # crear pila vacía
stack.append(10) # push
stack.append(20)
print(stack.pop()) # pop → 20
print(stack[-1]) # peek → 10
print(len(stack)) # size → 1
print(not stack) # is_empty → False
list.insert(0, item) o pop(0) porque su complejidad es O(n).
list en CPython usa un buffer contiguo con sobrerreserva; append puede realocar en crecimientos grandes.2.2. Usando collections.deque
El deque (double‑ended queue) está optimizado para inserciones y borrados en ambos extremos con complejidad O(1). Es la opción preferida cuando la pila puede crecer mucho o cuando se necesita también operar como cola.
# pila_deque.py
from collections import deque
stack = deque() # crear pila vacía
stack.append(1) # push
stack.append(2)
print(stack.pop()) # pop → 2
print(stack[-1]) # peek → 1
print(len(stack)) # size → 1
print(not stack) # is_empty → False
deque usa bloques enlazados; no requiere realocaciones contiguas, con uso de memoria estable en ampliaciones/disminuciones.2.3. Clase personalizada con tipado estático
Para proyectos grandes se recomienda encapsular la lógica en una clase que exponga una API clara y permita añadir validaciones, logs, límites y telemetría sin tocar el código cliente.
# stack_class.py
from __future__ import annotations
from typing import Generic, TypeVar, List, Iterable, Optional
T = TypeVar('T')
class Stack(Generic[T]):
"""Implementación genérica de una pila basada en list.
Provee métodos con nombre explícito y controla errores comunes.
"""
__slots__ = ('_items',)
def __init__(self, items: Optional[Iterable[T]] = None) -> None:
self._items: List[T] = list(items) if items is not None else []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
if self.is_empty():
raise IndexError('pop from empty stack')
return self._items.pop()
def peek(self) -> T:
if self.is_empty():
raise IndexError('peek from empty stack')
return self._items[-1]
def is_empty(self) -> bool:
return not self._items
def size(self) -> int:
return len(self._items)
def clear(self) -> None:
self._items.clear()
def __len__(self) -> int:
return len(self._items)
def __bool__(self) -> bool:
return not self.is_empty()
def __iter__(self):
# Itera desde la base a la cima
return iter(self._items)
def __repr__(self) -> str:
return f'Stack({self._items!r})'
# Uso
if __name__ == '__main__':
s = Stack[int]([5])
s.push(15)
print(s) # Stack([5, 15])
print(s.pop()) # 15
print(s.peek()) # 5
print(s.size()) # 1
Generic[T] obtienes ayuda de type checkers como mypy o pyright. En Python 3.12+, el tipado es más rápido y completo por mejoras de Faster CPython.
3. Comparativa de rendimiento
En la mayoría de los casos la diferencia entre list y deque es marginal, pero para pilas que alcanzan cientos de miles de elementos deque suele mostrar ventaja en memoria estable y en evitar picos por realocación de list.
| Implementación | Complejidad (push) | Complejidad (pop) | Uso típico | Ventajas clave |
|---|---|---|---|---|
list |
Amortizado O(1) | Amortizado O(1) | Scripts pequeños, prototipos | Simple, sin importación extra; excelente localidad de caché |
deque |
O(1) | O(1) | Aplicaciones de alto tráfico, servidores | Amortiguación de memoria estable; operaciones en ambos extremos |
| Clase personalizada | Depende de la colección interna | Depende de la colección interna | Proyectos con lógica de negocio | Encapsulamiento, tipado, logs, límites, telemetría |
3.1. Benchmark mínimo reproducible
# benchmark_stack.py
from collections import deque
import timeit
def bench(n=1_000_000):
t_list = timeit.timeit(
stmt='s.append(0); s.pop()',
setup='s=[]',
number=n
)
t_deque = timeit.timeit(
stmt='s.append(0); s.pop()',
setup='from collections import deque; s=deque()',
number=n
)
print(f'list: {t_list:.3f}s')
print(f'deque: {t_deque:.3f}s')
if __name__ == '__main__':
bench()
3.2. Perfilado de memoria con tracemalloc
# mem_profile.py
import tracemalloc
from collections import deque
def grow_stack(n, factory):
s = factory()
for i in range(n):
s.append(i)
return s
tracemalloc.start()
grow_stack(500_000, list)
snap_list = tracemalloc.take_snapshot()
tracemalloc.reset_peak()
grow_stack(500_000, deque)
snap_deque = tracemalloc.take_snapshot()
for title, snap in [('list', snap_list), ('deque', snap_deque)]:
top = snap.statistics('filename')[:1]
print(title, 'peak:', tracemalloc.get_traced_memory()[1], 'bytes')
tracemalloc.get_traced_memory() para detectar picos y filtrar fugas por archivo/línea.4. Casos de uso reales
- Evaluación de expresiones post‑fijo (RPN): la pila almacena operandos y opera cuando se encuentra un operador.
- Deshacer/rehacer en editores de texto: cada acción se apila; al deshacer se desapila.
- Recorrido iterativo de árboles (DFS): sustituto de la recursión para evitar desbordamiento de pila.
- Gestión de llamadas en intérpretes o máquinas virtuales: el call stack es esencial para lenguajes como Python.
- Algoritmos de backtracking (Sudoku, N‑Queens): se guardan estados parciales en una pila.
- Pila monotónica para next greater element o cálculo de histogramas.
- Cálculo de mínimos en O(1) con una pila auxiliar (MinStack).
4.1. Evaluador RPN robusto con manejo de errores
# rpn_evaluator.py
from stack_class import Stack
import operator
ops = {
'+': operator.add,
'-': operator.sub,
'*': operator.mul,
'/': operator.truediv,
}
def eval_rpn(tokens: list[str]) -> float:
s = Stack[float]()
for token in tokens:
if token in ops:
try:
b = s.pop()
a = s.pop()
except IndexError as e:
raise ValueError('expresión RPN inválida') from e
s.push(ops[token](a, b))
else:
try:
s.push(float(token))
except ValueError as e:
raise ValueError(f'token no numérico: {token!r}') from e
if s.size() != 1:
raise ValueError('expresión RPN mal formada')
return s.pop()
print(eval_rpn(['3', '4', '+', '2', '*', '7', '/'])) # 2.0
4.2. MinStack: mínimo en O(1)
class MinStack:
def __init__(self):
self._data = []
self._mins = []
def push(self, x):
self._data.append(x)
self._mins.append(x if not self._mins else min(x, self._mins[-1]))
def pop(self):
if not self._data:
raise IndexError('pop from empty stack')
self._mins.pop()
return self._data.pop()
def min(self):
if not self._mins:
raise IndexError('min from empty stack')
return self._mins[-1]
4.3. Pila monotónica para Next Greater Element (NGE)
def next_greater(nums: list[int]) -> list[int]:
res = [-1] * len(nums)
stack = [] # almacenará índices
for i, x in enumerate(nums):
while stack and nums[stack[-1]] < x:
j = stack.pop()
res[j] = x
stack.append(i)
return res
print(next_greater([2,1,2,4,3])) # [4,2,4,-1,-1]
5. Buenas prácticas y consideraciones de seguridad
5.1. Validación de entradas
Siempre verifica que la pila no esté vacía antes de ejecutar pop() o peek(). Lanzar IndexError permite a los consumidores manejar el error de forma explícita.
5.2. Evita estados mutables compartidos
Si la pila se comparte entre hilos, utiliza queue.LifoQueue o un threading.Lock para garantizar la atomicidad y coherencia.
5.3. Limita el tamaño máximo (opcional)
En entornos con recursos limitados, impón un máximo de capacidad para prevenir consumo excesivo de memoria.
from typing import TypeVar
T = TypeVar('T')
class BoundedStack(Stack[T]):
def __init__(self, limit: int):
super().__init__()
if limit <= 0:
raise ValueError('limit must be > 0')
self._limit = limit
def push(self, item: T) -> None:
if self.size() >= self._limit:
raise OverflowError('stack overflow: limit reached')
super().push(item)
5.4. Registro (logging) y auditoría
En sistemas productivos, registra push y pop con nivel DEBUG o INFO para auditoría sin afectar el rendimiento.
import logging
logging.basicConfig(level=logging.INFO)
class AuditStack(Stack[T]):
def push(self, item: T) -> None:
logging.debug('push %r', item)
super().push(item)
def pop(self) -> T:
item = super().pop()
logging.debug('pop %r', item)
return item
6. Depuración y troubleshooting comunes
- "IndexError: pop from empty stack" – ocurre cuando se llama a
pop()sin comprobaris_empty(). Solución: agregar guardia o usartry/except. - Rendimiento bajo en bucles intensivos – verifica que la pila no se esté recreando en cada iteración; reutiliza la instancia y mueve
append/popa variables locales para evitar attribute lookup dentro del bucle. - Consumo inesperado de memoria – usa
dequeoBoundedStacky perfila contracemalloc; evita referencias retenidas; para objetos grandes, consideradelygc.collect()en scripts largos. - Problemas de concurrencia – cambia a
queue.LifoQueuepara entornos multi‑thread o protege conthreading.Lock. Para procesos, usamultiprocessing.Manager().LifoQueue()o colas de procesos. - Desbordamiento por recursión – sustituye recursión por pila explícita (DFS iterativo) para evitar alcanzar
sys.getrecursionlimit().
7. Extensiones avanzadas
7.1. Pila persistente (immutability)
En programación funcional, una pila inmutable se implementa como una lista enlazada donde cada push devuelve una nueva pila sin modificar la anterior.
from __future__ import annotations
from dataclasses import dataclass
from typing import Generic, TypeVar, Optional
T = TypeVar('T')
@dataclass(frozen=True)
class PersistentStack(Generic[T]):
head: Optional[T] = None
tail: Optional['PersistentStack[T]'] = None
def push(self, value: T) -> 'PersistentStack[T]':
return PersistentStack(value, self)
def pop(self) -> 'PersistentStack[T]':
if self.tail is None:
raise IndexError('pop from empty persistent stack')
return self.tail
def peek(self) -> T:
if self.head is None:
raise IndexError('peek from empty persistent stack')
return self.head
Útil en backtracking cuando se requiere volver a estados previos sin copiar estructuras grandes.
7.2. Integración con asyncio (LIFO asíncrono)
asyncio no ofrece una LIFO nativa; implementa una con deque y asyncio.Condition para coordinar productores/consumidores.
import asyncio
from collections import deque
class AsyncLifo:
def __init__(self):
self._dq = deque()
self._cv = asyncio.Condition()
async def push(self, item):
async with self._cv:
self._dq.append(item)
self._cv.notify()
async def pop(self):
async with self._cv:
while not self._dq:
await self._cv.wait()
return self._dq.pop()
async def producer(q: AsyncLifo):
for i in range(5):
await q.push(i)
print(f'produced {i}')
await asyncio.sleep(0.1)
async def consumer(q: AsyncLifo):
for _ in range(5):
item = await q.pop()
print(f'consumed {item}')
async def main():
q = AsyncLifo()
await asyncio.gather(producer(q), consumer(q))
# asyncio.run(main())
8. Resumen rápido (cheat‑sheet)
Operaciones en Python
stack.append(x)– pushstack.pop()– popstack[-1]– peeklen(stack)– sizenot stack– is_empty
Cuándo usar cada implementación
list | Prototipos, código simple |
deque | Altas tasas de inserción/borrado, uso de ambos extremos |
| Clase personalizada | Proyectos grandes, tipado, logging, límites |
queue.LifoQueue | Multi‑thread con bloqueo y coordinación |
9. Compatibilidad y requisitos del sistema
9.1. Versiones de Python
- Python 3.8+:
typingmaduro yfrom __future__ import annotations. - Python 3.10+: tipos nativos de unión (
int | str), mejor rendimiento. - Python 3.11/3.12: Faster CPython (10–25% más veloz en promedio); recomendable para cargas intensivas.
9.2. SO y dependencias
- SO soportados: Linux, macOS, Windows (x86_64/ARM64).
- Biblioteca estándar:
collections,queue,typing,asyncio. - No se requieren dependencias externas para las funciones básicas.
Recomendación: usa entornos virtuales:
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip mypy pytest
10. Optimización y benchmarking
10.1. Micro-optimizaciones prácticas
- Evita attribute lookup en bucles: asigna
append = stack.appendypop = stack.pop. - Prefiere
listpara pilas puras por mejor localidad; usadequesi esperas oscilaciones grandes. - Reserva con
list.extendsi conoces el tamaño aproximado para minimizar realocaciones.
append, pop = stack.append, stack.pop
for _ in range(n):
append(x)
pop()
10.2. Medición confiable
- Usa
timeitpara micro-benchmarks. - Perfila CPU con
cProfiley analiza consnakeviz. - Perfila memoria con
tracemalloc.
python -m cProfile -o profile.out your_script.py
snakeviz profile.out
11. Concurrencia y paralelismo
11.1. Multi‑thread
import threading
from queue import LifoQueue
stack = LifoQueue(maxsize=0) # ilimitado
def worker():
stack.put(1) # push
item = stack.get() # pop
stack.task_done()
t = threading.Thread(target=worker)
t.start(); t.join()
LifoQueue o Lock para operaciones múltiples.11.2. Multi‑process
from multiprocessing import Process, Manager
with Manager() as m:
stack = m.LifoQueue()
def worker():
stack.put('x')
print(stack.get())
p = Process(target=worker)
p.start(); p.join()
12. Pruebas, calidad y contratos
12.1. Unit tests con pytest
# test_stack.py
from stack_class import Stack
import pytest
def test_push_pop():
s = Stack[int]()
s.push(1); s.push(2)
assert s.pop() == 2
assert s.pop() == 1
with pytest.raises(IndexError):
s.pop()
pytest -q
12.2. Tipado estático y CI
mypy stack_class.py
pyright
13. Despliegue en contenedores: Docker vs Podman
Para reproducibilidad y portabilidad, ejecuta tus scripts/benchmarks en contenedores. Docker y Podman ofrecen entornos consistentes; Podman es rootless por defecto y compatible con CLI de Docker.
13.1. Dockerfile minimal
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir -U pip pytest mypy
CMD ["python", "benchmark_stack.py"]
docker build -t python-stack:latest .
docker run --rm -it python-stack:latest
13.2. Podman (rootless)
podman build -t python-stack:latest .
podman run --rm -it localhost/python-stack:latest
- Seguridad: Podman sin daemon con privilegios; mejor integración rootless.
- Compatibilidad: CLI y Dockerfiles mayormente compatibles.
- Rendimiento: similar en Linux; en macOS/Windows depende de la VM subyacente.
--cpus y --memory para simular límites.
14. Comparativa con otros lenguajes
Java
Deque<T>(p. ej.,ArrayDeque) recomendado.Stackestá obsoleto para nuevos desarrollos.
C++
std::stack<T>adaptador sobrestd::vectorodeque.- Control explícito de memoria y tipos; rendimiento máximo.
Go / Rust
- Go: usa slices como pila con
append/popmanual. - Rust:
Vec<T>conpush/popy garantías de seguridad en compilación.
Python ofrece rapidez de desarrollo y una biblioteca estándar rica; para secciones críticas, considera extensiones en C, Cython o Rust (PyO3).
15. FAQ
LifoQueue o bloqueos explícitos para seguridad en concurrencia.list.pickle para Python‑a‑Python o convierte a lista para JSON: json.dumps(list(stack)). Considera seguridad: no cargues pickle de fuentes no confiables.16. Referencias y buenas prácticas de la industria
- Python Docs – Estructuras de datos
- Python Docs –
collections.deque - Python Docs –
queue.LifoQueue - PEP 659 – Especialización adaptativa (Faster CPython)
- Testing – pytest, Tipado – mypy
- Contenedores – Docker y Podman
Guía Completa de la Estructura de Pila (Stack) en Python: Conceptos, Implementaciones y Buenas Prácticas