collections en Python: Counter, defaultdict, deque, ChainMap y OrderedDict

El módulo collections ofrece alternativas especializadas a las estructuras de datos built-in de Python. Cada una resuelve un patrón concreto de forma más eficiente o más expresiva que usar listas y diccionarios genéricos.

Counter — contar frecuencias

Counter es un diccionario especializado en contar: las claves son elementos y los valores son su recuento. Acepta cualquier iterable y tiene métodos propios para las operaciones más comunes sobre frecuencias.

from collections import Counter

palabras = ["python", "java", "python", "rust", "python", "java"]
conteo = Counter(palabras)
print(conteo)  # Counter({'python': 3, 'java': 2, 'rust': 1})

# Acceso como dict normal
print(conteo["python"])  # 3
print(conteo["go"])      # 0 (no KeyError)

# Las N más comunes
print(conteo.most_common(2))  # [('python', 3), ('java', 2)]

# Aritmética entre contadores
c1 = Counter("aabbcc")
c2 = Counter("abcdd")
print(c1 + c2)  # Counter({'a': 3, 'b': 3, 'c': 3, 'd': 2})
print(c1 - c2)  # Counter({'a': 1, 'b': 1, 'c': 1})
from collections import Counter

# Análisis de texto
texto = "El rápido zorro marrón salta sobre el perro perezoso"
conteo_letras = Counter(texto.lower().replace(" ", ""))
print(conteo_letras.most_common(5))
# [('o', 7), ('r', 6), ('e', 5), ('l', 3), ('a', 3)]

# Actualizar con más datos
log1 = Counter(["INFO", "ERROR", "INFO", "WARNING"])
log2 = Counter(["ERROR", "ERROR", "INFO"])
log1.update(log2)
print(log1)  # Counter({'INFO': 3, 'ERROR': 3, 'WARNING': 1})

defaultdict — evitar KeyError con valores por defecto

defaultdict(callable) genera automáticamente un valor por defecto cuando accedes a una clave inexistente, en lugar de lanzar KeyError.

from collections import defaultdict

# Agrupar por categoría sin comprobar si la clave existe
ventas = [
    ("frutas", "manzana"), ("verduras", "zanahoria"),
    ("frutas", "pera"), ("frutas", "uva"), ("verduras", "brócoli"),
]

por_categoria: dict[str, list] = defaultdict(list)
for categoria, producto in ventas:
    por_categoria[categoria].append(producto)

print(dict(por_categoria))
# {'frutas': ['manzana', 'pera', 'uva'], 'verduras': ['zanahoria', 'brócoli']}

# Sin defaultdict necesitarías:
# if categoria not in por_categoria:
#     por_categoria[categoria] = []
# por_categoria[categoria].append(producto)
from collections import defaultdict

# Grafo como lista de adyacencia
grafo: dict[str, list] = defaultdict(list)
aristas = [("A", "B"), ("A", "C"), ("B", "D"), ("C", "D")]
for origen, destino in aristas:
    grafo[origen].append(destino)
print(dict(grafo))
# {'A': ['B', 'C'], 'B': ['D'], 'C': ['D']}

# defaultdict anidado
from collections import defaultdict
matriz = defaultdict(lambda: defaultdict(int))
matriz["fila1"]["col1"] += 1
matriz["fila1"]["col2"] += 5
print(dict(matriz["fila1"]))  # {'col1': 1, 'col2': 5}

deque — cola de doble extremo O(1)

Las listas de Python tienen pop(0) en O(n): desplaza todos los elementos. deque hace appendleft() y popleft() en O(1) porque usa una lista doblemente enlazada.

from collections import deque

# Cola FIFO eficiente
cola: deque = deque()
cola.append("tarea-1")    # añadir al final
cola.append("tarea-2")
cola.appendleft("urgente")  # añadir al inicio
print(cola.popleft())  # 'urgente' — O(1)
print(cola.popleft())  # 'tarea-1'

# Ventana deslizante de tamaño N
def media_movil(datos: list[float], n: int) -> list[float]:
    ventana: deque = deque(maxlen=n)
    medias = []
    for valor in datos:
        ventana.append(valor)
        if len(ventana) == n:
            medias.append(sum(ventana) / n)
    return medias

precios = [10, 12, 11, 14, 13, 15, 16, 14]
print(media_movil(precios, 3))
# [11.0, 12.33, 12.67, 14.0, 14.67, 15.0]

ChainMap — buscar en varios dicts sin copiarlos

ChainMap agrupa varios diccionarios en una vista única. La búsqueda recorre los dicts en orden hasta encontrar la clave: ideal para implementar capas de configuración.

from collections import ChainMap

# Capas de configuración: env > config.json > defaults
defaults = {"debug": False, "timeout": 30, "host": "localhost", "puerto": 8080}
config_fichero = {"timeout": 60, "host": "db.prod.com"}
config_env = {"debug": True}

config = ChainMap(config_env, config_fichero, defaults)
print(config["debug"])    # True  (de config_env)
print(config["host"])     # db.prod.com  (de config_fichero)
print(config["puerto"])   # 8080  (de defaults)

# Las modificaciones afectan solo al primer dict
config["nuevo_param"] = "valor"
print(list(config.maps[0].items()))  # [('debug', True), ('nuevo_param', 'valor')]

OrderedDict — con move_to_end para cachés LRU

from collections import OrderedDict

class LRUCache:
    """Caché LRU con capacidad máxima usando OrderedDict."""

    def __init__(self, capacidad: int):
        self._cache: OrderedDict = OrderedDict()
        self._capacidad = capacidad

    def get(self, clave: str):
        if clave not in self._cache:
            return None
        self._cache.move_to_end(clave)  # marcar como recién usado
        return self._cache[clave]

    def put(self, clave: str, valor) -> None:
        if clave in self._cache:
            self._cache.move_to_end(clave)
        self._cache[clave] = valor
        if len(self._cache) > self._capacidad:
            self._cache.popitem(last=False)  # eliminar el menos reciente

cache = LRUCache(3)
cache.put("a", 1)
cache.put("b", 2)
cache.put("c", 3)
cache.get("a")     # 'a' se mueve al final
cache.put("d", 4)  # expulsa 'b', el menos reciente
print(list(cache._cache.keys()))  # ['c', 'a', 'd']

La guía rápida: Counter para frecuencias y votaciones; defaultdict para agrupar sin comprobar existencia de clave; deque cuando necesitas añadir o quitar del inicio frecuentemente o implementar una ventana deslizante; ChainMap para configuración en capas; OrderedDict para estructuras donde el orden de inserción importa y necesitas reordenar sin recrear el dict.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP