El módulo collections de la biblioteca estándar de Python es uno de los conjuntos de herramientas más valiosos para cualquier desarrollador. Ofrece tipos de datos especializados que van más allá de los tipos nativos como listas, tuplas y diccionarios, resolviendo problemas comunes de forma elegante y eficiente. Según la documentación oficial de Python para collections, este módulo implementa tipos alternativos de contenedores de datos con características específicas para diferentes escenarios.
Si alguna vez has necesitado contar elementos de una lista, crear una cola eficiente o acceder a múltiples diccionarios como si fueran uno solo, el módulo collections tiene la solución ideal. Complementa perfectamente los diccionarios en Python y las tuplas y conjuntos en Python, que son estructuras fundamentales del lenguaje. En esta guía completa, aprenderás cada uno de los componentes principales del módulo collections con ejemplos prácticos y casos de uso reales.
¿Qué es el módulo collections?
El módulo collections fue introducido en Python 2.4 y se ha expandido significativamente a lo largo de los años. Proporciona alternativas a los tipos de datos nativos para situaciones donde las listas, tuplas y diccionarios comunes no son suficientes o eficientes. Cada clase del módulo resuelve un problema específico: mantener el orden de inserción, proporcionar valores predeterminados para claves ausentes, crear objetos ligeros similares a tuplas con campos nombrados, entre otros.
La guía de Real Python sobre collections ofrece una excelente introducción a los conceptos fundamentales, clasificando cada estructura de datos según su caso de uso ideal. El módulo está implementado puramente en Python, con optimizaciones en C para máximo rendimiento, como puedes ver en el código fuente oficial de CPython en GitHub.
Counter: contando elementos como un profesional
La clase Counter es una subclase de dict diseñada específicamente para contar objetos hashables. Almacena los elementos como claves y sus recuentos como valores, haciendo trivial responder preguntas como "¿qué palabra aparece con más frecuencia en este texto?" o "¿cuántas veces aparece este número en la lista?".
Creando y usando un Counter
from collections import Counter
Desde una lista
frutas = ['manzana', 'banana', 'manzana', 'naranja', 'banana', 'manzana']
contador = Counter(frutas)
print(contador)
Counter({'manzana': 3, 'banana': 2, 'naranja': 1})
Desde una cadena
letras = Counter("paralelepipedo")
print(letras)
Counter({'p': 3, 'e': 3, 'a': 2, 'l': 2, 'r': 1, 'i': 1, 'd': 1, 'o': 1})
Desde un diccionario
contador = Counter({'a': 4, 'b': 2, 'c': 1})
Métodos esenciales del Counter
El Counter ofrece métodos que van mucho más allá del simple conteo. El método most_common() devuelve los n elementos más frecuentes, ideal para rankings:
from collections import Counter
ventas = Counter(['iphone', 'iphone', 'samsung', 'iphone', 'xiaomi', 'samsung'])
print(ventas.most_common(2))
[('iphone', 3), ('samsung', 2)]
Las operaciones aritméticas entre Counters tienen soporte nativo:
c1 = Counter(a=3, b=1, c=2)
c2 = Counter(a=1, b=2, c=3)
print(c1 + c2) # Suma: Counter({'c': 5, 'a': 4, 'b': 3})
print(c1 - c2) # Resta: Counter({'a': 2}) (solo positivos)
print(c1 & c2) # Intersección (mínimo): Counter({'a': 1, 'b': 1, 'c': 2})
print(c1 | c2) # Unión (máximo): Counter({'c': 3, 'a': 3, 'b': 2})
La clase Counter se utiliza ampliamente en procesamiento de lenguaje natural, análisis de logs y cualquier escenario que requiera conteo de frecuencias. Consulta la documentación oficial sobre objetos Counter para obtener detalles completos de la API.
defaultdict: diccionarios con valores predeterminados
¿Cuántas veces has escrito código como este?
diccionario = {}
for clave, valor in datos:
if clave not in diccionario:
diccionario[clave] = []
diccionario[clave].append(valor)
El defaultdict elimina esta repetición. Es una subclase de dict que llama a una función factory para proporcionar valores predeterminados cuando se accede a una clave inexistente:
from collections import defaultdict
defaultdict con list como factory
datos = [('a', 1), ('b', 2), ('a', 3), ('c', 4), ('b', 5)]
dd = defaultdict(list)
for clave, valor in datos:
dd[clave].append(valor)
print(dict(dd))
{'a': [1, 3], 'b': [2, 5], 'c': [4]}
Usos comunes del defaultdict
Las factories más utilizadas con defaultdict son list, set, int y dict:
from collections import defaultdict
defaultdict(int) para conteo automático
conteo = defaultdict(int)
for palabra in ["uno", "dos", "uno", "tres", "uno", "dos"]:
conteo[palabra] += 1
print(dict(conteo))
{'uno': 3, 'dos': 2, 'tres': 1}
defaultdict(set) para conjuntos
agrupacion = defaultdict(set)
agrupacion['pares'].add(2)
agrupacion['pares'].add(4)
agrupacion['impares'].add(1)
print(dict(agrupacion))
{'pares': {2, 4}, 'impares': {1}}
defaultdict(dict) para diccionarios anidados
anidado = defaultdict(dict)
anidado['user1']['nombre'] = 'Ana'
anidado['user2']['nombre'] = 'Bob'
print(dict(anidado))
{'user1': {'nombre': 'Ana'}, 'user2': {'nombre': 'Bob'}}
El defaultdict es especialmente útil al procesar datos agrupados y construir estructuras anidadas. Consulta la documentación oficial sobre defaultdict para más ejemplos y casos de uso avanzados.
namedtuple: tuplas con nombres de campos
La función namedtuple() crea clases de tupla cuyos campos pueden accederse tanto por índice como por nombre. Esto combina la inmutabilidad y eficiencia de las tuplas con la legibilidad de los diccionarios:
from collections import namedtuple
Definiendo una namedtuple
Punto = namedtuple('Punto', ['x', 'y'])
p1 = Punto(10, 20)
p2 = Punto(x=30, y=40)
print(p1.x, p1.y) # 10 20 (acceso por nombre)
print(p2[0], p2[1]) # 30 40 (acceso por índice)
Representación limpia
print(p1) # Punto(x=10, y=20)
Namedtuples en aplicaciones reales
Las namedtuples son ideales para representar registros ligeros sin la sobrecarga de una clase completa. Son inmutables, consumen menos memoria que los diccionarios y son más legibles que las tuplas comunes:
from collections import namedtuple
Registro de empleado
Empleado = namedtuple('Empleado', ['id', 'nombre', 'cargo', 'salario'])
empleados = [
Empleado(1, 'Ana García', 'Ingeniera de Datos', 12000),
Empleado(2, 'Carlos López', 'Científico de Datos', 15000),
Empleado(3, 'María Torres', 'Desarrolladora Python', 10000),
]
Filtrar con comprensión de lista
ingenieros = [e for e in empleados if 'Datos' in e.cargo]
print(ingenieros[0].nombre) # Ana García
Además de los campos nombrados, namedtuple ofrece el método _asdict() para convertir a diccionario y _replace() para crear una nueva instancia con campos modificados:
p = Punto(10, 20)
print(p._asdict()) # {'x': 10, 'y': 20}
p2 = p._replace(x=50)
print(p2) # Punto(x=50, y=20)
Según la documentación oficial sobre namedtuple, esta función es especialmente útil para reemplazar tuplas comunes cuando la legibilidad del código es importante.
deque: colas y pilas eficientes
La clase deque (double-ended queue) es una lista optimizada para inserciones y eliminaciones en ambos extremos. Mientras que una lista Python tiene complejidad O(n) para insertar o eliminar al inicio, deque ofrece O(1) para estas operaciones:
from collections import deque
Creando un deque
cola = deque(['a', 'b', 'c'])
Agregar al final
cola.append('d')
print(cola) # deque(['a', 'b', 'c', 'd'])
Agregar al inicio
cola.appendleft('z')
print(cola) # deque(['z', 'a', 'b', 'c', 'd'])
Eliminar del final
ultimo = cola.pop()
print(ultimo) # d
Eliminar del inicio
primero = cola.popleft()
print(primero) # z
Rotando y limitando el deque
El deque ofrece dos funcionalidades poderosas: rotación y tamaño máximo:
from collections import deque
Rotación (útil para juegos y algoritmos circulares)
d = deque([1, 2, 3, 4, 5])
d.rotate(2)
print(d) # deque([4, 5, 1, 2, 3])
d.rotate(-1)
print(d) # deque([5, 1, 2, 3, 4])
Tamaño máximo (buffer circular automático)
buffer = deque(maxlen=3)
buffer.append(1)
buffer.append(2)
buffer.append(3)
print(buffer) # deque([1, 2, 3], maxlen=3)
buffer.append(4) # Elimina automáticamente el elemento más antiguo
print(buffer) # deque([2, 3, 4], maxlen=3)
El deque es la estructura ideal para implementar colas de tareas, historial de navegación (con maxlen), buffers circulares y algoritmos de sliding window. Consulta la documentación oficial sobre objetos deque para una visión completa de todos los métodos disponibles.
OrderedDict: diccionarios con orden garantizado
Antes de Python 3.7, los diccionarios comunes no garantizaban el orden de inserción. OrderedDict fue creado para llenar este vacío. Hoy en día, con diccionarios nativos ordenados, su principal diferenciador es el método move_to_end():
from collections import OrderedDict
od = OrderedDict()
od['a'] = 1
od['b'] = 2
od['c'] = 3
od['d'] = 4
Mover la clave 'a' al final
od.move_to_end('a')
print(od)
OrderedDict([('b', 2), ('c', 3), ('d', 4), ('a', 1)])
Mover 'd' al inicio
od.move_to_end('d', last=False)
print(od)
OrderedDict([('d', 4), ('b', 2), ('c', 3), ('a', 1)])
Además, OrderedDict considera el orden al comparar igualdad, a diferencia de los diccionarios comunes:
from collections import OrderedDict
od1 = OrderedDict([('a', 1), ('b', 2)])
od2 = OrderedDict([('b', 2), ('a', 1)])
print(od1 == od2) # False (¡el orden importa!)
dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 2, 'a': 1}
print(dict1 == dict2) # True (el orden no importa)
OrderedDict es ideal para implementar cachés LRU (Least Recently Used) y cualquier estructura que necesite mantener registro del orden de acceso o inserción. Consulta la documentación oficial sobre OrderedDict para más detalles y ejemplos.
ChainMap: múltiples diccionarios en uno solo
La clase ChainMap agrupa múltiples diccionarios o mapeos en una única vista buscable. Las búsquedas recorren los diccionarios en el orden en que fueron pasados, devolviendo el primer valor encontrado:
from collections import ChainMap
Configuración con precedencia
predeterminados = {'tema': 'claro', 'idioma': 'es-ES', 'notificaciones': True}
usuario = {'idioma': 'en-US', 'notificaciones': False}
entorno = {'tema': 'oscuro'}
config = ChainMap(entorno, usuario, predeterminados)
print(config['tema']) # 'oscuro' (de entorno)
print(config['idioma']) # 'en-US' (de usuario)
print(config['notificaciones']) # False (de usuario)
Las actualizaciones afectan solo al primer diccionario
config['tema'] = 'alto-contraste'
print(entorno['tema']) # 'alto-contraste'
ChainMap es extremadamente útil para gestionar configuraciones con diferentes niveles de precedencia (predeterminado → usuario → entorno), procesar argumentos de línea de comandos combinados con valores por defecto, y ámbitos de variables en intérpretes:
from collections import ChainMap
Ámbito de variables simulando un intérprete
ambito_global = {'x': 10, 'y': 20, 'nombre': 'global'}
ambito_local = {'x': 5, 'z': 30}
ambito = ChainMap(ambito_local, ambito_global)
print(ambito['x']) # 5 (local)
print(ambito['y']) # 20 (global)
print(ambito['z']) # 30 (local)
Agregar nuevo ámbito
ambito = ambito.new_child({'x': 1, 'w': 100})
print(ambito['x']) # 1 (nuevo ámbito local)
Según la documentación oficial sobre ChainMap, esta clase es particularmente útil cuando necesitas gestionar múltiples namespaces sin fusionarlos.
Otras herramientas del módulo collections
Además de las clases principales, el módulo collections ofrece otras herramientas valiosas:
UserDict, UserList y UserString
Estas clases son wrappers que facilitan la creación de subclases de dict, list y string. A diferencia de heredar directamente de estos tipos nativos, estas clases exponen atributos como .data para acceder al contenido interno, simplificando la personalización:
from collections import UserDict
class DiccionarioMinusculas(UserDict):
def setitem(self, clave, valor):
clave = clave.lower()
super().setitem(clave, valor)
d = DiccionarioMinusculas()
d['Nombre'] = 'Ana'
print(d.data) # {'nombre': 'Ana'}
Buenas prácticas con collections
Para aprovechar al máximo el módulo collections, considera las siguientes recomendaciones:
- Usa Counter en lugar de implementar tu propia lógica de conteo con diccionarios — el código es más legible y eficiente
- Prefiere defaultdict siempre que necesites verificar si una clave existe antes de accederla; esto elimina bloques
if clave in dictrepetitivos - Elige namedtuple sobre tuplas comunes cuando los datos tengan significado semántico; tu código se vuelve autodocumentado
- Utiliza deque para colas y pilas en lugar de listas cuando haya operaciones frecuentes al inicio de la colección
- Recurre a OrderedDict cuando el orden de inserción importe para la lógica de tu algoritmo
- Adopta ChainMap para gestionar configuraciones con múltiples capas de precedencia sin fusionar diccionarios
Rendimiento: collections vs tipos nativos
Un aspecto frecuentemente subestimado es la ganancia de rendimiento al usar las clases correctas del módulo collections. Aquí tienes una comparación práctica:
from collections import deque, Counter, defaultdict
import time
deque vs list para inserción al inicio
n = 100000
lista = []
inicio = time.time()
for i in range(n):
lista.insert(0, i)
print(f"list.insert(0): {time.time() - inicio:.3f}s")
dq = deque()
inicio = time.time()
for i in range(n):
dq.appendleft(i)
print(f"deque.appendleft: {time.time() - inicio:.3f}s")
Resultado típico: deque es 100x más rápido
El módulo collections implementa cada clase con la estructura de datos más adecuada para su propósito. deque, por ejemplo, se implementa como un array de bloques fijos (double-ended queue), mientras que Counter hereda la implementación optimizada en C del dict nativo. Para benchmarks detallados, consulta la guía de rendimiento de la documentación oficial.
Conclusión
El módulo collections es una de las bibliotecas más útiles de la stdlib de Python. Proporciona soluciones elegantes y eficientes para problemas recurrentes de programación: conteo de frecuencias con Counter, valores predeterminados con defaultdict, registros nombrados con namedtuple, colas eficientes con deque, orden garantizado con OrderedDict y ámbitos encadenados con ChainMap.
Dominar estas herramientas no solo hace que tu código sea más limpio y legible, sino que también mejora significativamente el rendimiento de tus aplicaciones. Cada clase fue diseñada para un conjunto específico de problemas, y saber cuál elegir es una señal de un desarrollador Python experimentado.
Para continuar tus estudios, explora la documentación oficial completa del módulo collections y practica implementando los ejemplos de esta guía en tus propios proyectos. El conocimiento profundo de las herramientas estándar de Python es uno de los mayores diferenciadores de un programador eficiente.