4 - Representación de una pila con listas

Modelo con list como almacenamiento

La forma más directa de materializar una pila en Python es apoyarse en una lista nativa y tratar su última posición como tope. Las listas crecen dinámicamente, ofrecen append y pop en O(1) amortizado y conservan el orden de inserción sin que tengamos que manejar memoria manualmente.

Ubicar el tope al final evita desplazar elementos: cada operación solo incrementa o decrementa la longitud de la lista, respetando la complejidad constante esperada de un TDA pila.

4.1 Componentes mínimos

  • Buffer: una lista privada _datos que almacena los elementos.
  • Tope implícito: corresponde al último índice disponible (-1).
  • Capacidad opcional: límite máximo de elementos para escenarios donde la memoria debe acotarse.
class PilaLista:
  def __init__(self, capacidad=None):
    self._datos = []
    self._capacidad = capacidad

  def __len__(self):
    return len(self._datos)

  def esta_vacia(self):
    return len(self._datos) == 0

  def push(self, valor):
    if self._capacidad is not None and len(self._datos) >= self._capacidad:
      raise OverflowError("Pila llena")
    self._datos.append(valor)

  def pop(self):
    if self.esta_vacia():
      raise IndexError("Pila vacia")
    return self._datos.pop()

  def peek(self):
    if self.esta_vacia():
      raise IndexError("Pila vacia")
    return self._datos[-1]

El contrato expone solo las operaciones propias de una pila. Si necesitamos recorrer los elementos, podemos iterar sobre una copia (list(self._datos)) para no romper el orden LIFO.

4.2 ¿Por qué el tope va al final?

Manipular el extremo derecho de la lista evita desplazamientos internos. Si usáramos el índice cero, cada pop(0) debería reacomodar todos los elementos restantes. Con append/pop el costo se mantiene acotado y la localidad de memoria mejora el rendimiento práctico.

Este enfoque también coincide con collections.deque, lo que facilita intercambiar implementaciones sin cambiar la interfaz pública.

4.3 Capacidad y manejo de errores

La capacidad es opcional pero recomendable cuando la pila respalda buffers de red, historiales acotados o límites de negocio. Se sugieren estas reglas:

  • Overflow: lanzar OverflowError si un push supera el límite.
  • Underflow: lanzar IndexError ante un pop/peek en pila vacía.
  • Consultas auxiliares: exponer esta_vacia y __len__ para facilitar validaciones previas.
pila = PilaLista(capacidad=2)

try:
  pila.push("A")
  pila.push("B")
  pila.push("C")  # lanza OverflowError
except OverflowError as exc:
  print("No se pudo apilar:", exc)

while not pila.esta_vacia():
  print("Desapilado:", pila.pop())

# pila.pop()  # IndexError si se descomenta

Usar excepciones idiomáticas mantiene el código Pythonico y permite a las aplicaciones cliente decidir si registrar, reintentar o mostrar un mensaje al usuario.

4.4 Intercambiar list por deque sin romper la interfaz

Si se requiere rendimiento O(1) estricto ante miles de operaciones, basta con reemplazar el buffer por un deque. El resto de la clase permanece igual, respetando el encapsulamiento del TDA.

from collections import deque


class PilaLista:
  def __init__(self, usar_deque=False, capacidad=None):
    self._capacidad = capacidad
    self._datos = deque() if usar_deque else []

  # ... mismos metodos push, pop, peek y esta_vacia ...

Gracias a este detalle, cambiar de list a deque no implica reescribir las llamadas externas. Solo el constructor conoce la estructura concreta.

Prueba la versión con lista o con deque directamente en el editor online:

Abrir editor online

4.5 Invariantes y depuración rápida

  • Longitud coherente: len(self._datos) refleja la cantidad real de elementos.
  • Tope siempre accesible: si esta_vacia() es falso, self._datos[-1] existe.
  • Capacidad establecida: si _capacidad es un entero, no se permiten inserciones adicionales.

Comprobar rápidamente estas condiciones ayuda a encontrar errores de lógica antes de avanzar hacia operaciones más complejas que veremos en el tema siguiente.