Módulo collections de Python
Aprende el módulo collections de Python: Counter, defaultdict, namedtuple, deque, OrderedDict y ChainMap, con ejemplos prácticos y cuándo usar cada uno.
El módulo collections incorporado de Python proporciona tipos de contenedor especializados que extienden o reemplazan la lista, el dict y la tupla estándar. Cada tipo resuelve un problema específico que, de otro modo, requeriría varias líneas adicionales de gestión manual.
Este capítulo cubre los seis tipos de uso más común: Counter, defaultdict, namedtuple, deque, OrderedDict y ChainMap. Para cada uno verás qué problema resuelve, cómo crearlo y usarlo, y los errores típicos que hay que tener en cuenta.
No se necesita instalación — collections viene incluido con cada instalación de Python 3:
from collections import Counter, defaultdict, namedtuple, deque, OrderedDict, ChainMapCounter
Counter es una subclase de dict diseñada para contar objetos hashables. Le pasas un iterable (o una cadena, o argumentos con nombre) y devuelve un objeto similar a un diccionario donde las claves son los elementos y los valores son sus recuentos.
Creación de un Counter
from collections import Counter
# From a list
word_list = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
c = Counter(word_list)
print(c)
# Output: Counter({'apple': 3, 'banana': 2, 'cherry': 1})Las claves que no existen devuelven 0 en lugar de lanzar un KeyError:
print(c['apple']) # 3
print(c['mango']) # 0 — no KeyErrorElementos más frecuentes
most_common(n) devuelve los n elementos con mayor recuento como una lista de tuplas (elemento, recuento), ordenados de mayor a menor frecuencia:
print(c.most_common(2))
# Output: [('apple', 3), ('banana', 2)]Omite n para obtener todos los elementos ordenados por frecuencia.
Aritmética de Counter
Los Counters admiten suma, resta, intersección y unión:
a = Counter(['a', 'a', 'b']) # Counter({'a': 2, 'b': 1})
b = Counter(['a', 'b', 'b', 'c']) # Counter({'b': 2, 'a': 1, 'c': 1})
print(a + b) # Counter({'a': 3, 'b': 3, 'c': 1})
print(a - b) # Counter({'a': 1}) — only positive counts kept
print(a & b) # Counter({'a': 1, 'b': 1}) — minimum of each count
print(a | b) # Counter({'a': 2, 'b': 2, 'c': 1}) — maximum of each countCuándo usar Counter: recuento de votos, frecuencias de palabras, recuentos de caracteres, construcción de histogramas.
Trampa del Counter: la resta conserva solo los positivos
a - b descarta silenciosamente los elementos donde el resultado sería cero o negativo. Si necesitas conservar recuentos negativos, usa subtract() en su lugar:
a = Counter({'x': 2})
b = Counter({'x': 5})
a.subtract(b)
print(a) # Counter({'x': -3}) — negative count preserveddefaultdict
defaultdict es una subclase de dict que llama a una función de fábrica para proporcionar un valor por defecto cada vez que accedes a una clave que aún no existe. Esto elimina la necesidad de cláusulas de guarda del tipo if key not in d:.
Creación de un defaultdict
Pasa la fábrica como primer argumento:
from collections import defaultdict
dd = defaultdict(int) # default value: int() == 0
words = ['cat', 'dog', 'cat', 'bird', 'dog', 'cat']
for word in words:
dd[word] += 1 # no KeyError on first access
print(dict(dd))
# Output: {'cat': 3, 'dog': 2, 'bird': 1}Sin defaultdict necesitarías dd[word] = dd.get(word, 0) + 1 o un Counter.
Agrupación de elementos con list como fábrica
groups = defaultdict(list)
data = [('fruit', 'apple'), ('veggie', 'carrot'), ('fruit', 'banana'), ('veggie', 'broccoli')]
for category, item in data:
groups[category].append(item)
print(dict(groups))
# Output: {'fruit': ['apple', 'banana'], 'veggie': ['carrot', 'broccoli']}Funciones de fábrica comunes
| Fábrica | Valor por defecto | Uso típico |
|---|---|---|
int | 0 | Conteo |
float | 0.0 | Acumulación de sumas |
list | [] | Agrupación de elementos |
set | set() | Recolección de valores únicos |
str | '' | Construcción de cadenas |
dict | {} | Mapeos anidados |
También puedes pasar una lambda sin argumentos para un valor por defecto personalizado: defaultdict(lambda: 'N/A').
Trampa del defaultdict: acceder a una clave la crea
A diferencia de dict.get(), un simple acceso dd[key] sobre una clave inexistente inserta esa clave con el valor por defecto. Esto puede sorprenderte al iterar o comprobar la pertenencia:
dd = defaultdict(int)
print('foo' in dd) # False — key does not exist yet
_ = dd['foo'] # access inserts the key
print('foo' in dd) # True — key was silently createdUsa dd.get('foo') o 'foo' in dd cuando quieras comprobar sin efectos secundarios.
Cuándo usar defaultdict: agrupación de datos, construcción de listas de adyacencia para grafos, cualquier patrón donde inicializas y luego actualizas.
namedtuple
namedtuple crea una nueva clase cuyas instancias son como tuplas normales pero con campos con nombre. El resultado es inmutable, eficiente en memoria (sin __dict__ por instancia) y autodocumentado.
Creación de un namedtuple
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(3, 7)
print(p) # Point(x=3, y=7)
print(p.x) # 3
print(p.y) # 7
print(p[0]) # 3 — index access still worksEl primer argumento de namedtuple() es el nombre del tipo (usado en repr). El segundo argumento es una lista de nombres de campo (o una cadena separada por espacios o comas: 'x y').
Ejemplo práctico
Employee = namedtuple('Employee', ['name', 'department', 'salary'])
emp = Employee('Alice', 'Engineering', 95000)
print(emp.name, emp.department, emp.salary)
# Output: Alice Engineering 95000El acceso por nombre (emp.name) es mucho más claro que el acceso posicional (row[0]) al leer datos de archivos CSV o filas de base de datos.
Métodos útiles de namedtuple
# Convert to an ordered dictionary
print(p._asdict()) # {'x': 3, 'y': 7}
# Create a modified copy (namedtuples are immutable)
p2 = p._replace(x=10)
print(p2) # Point(x=10, y=7)
print(p) # Point(x=3, y=7) — original unchangednamedtuple vs dataclass
Python 3.7 introdujo dataclasses.dataclass como alternativa. Elige namedtuple cuando quieras inmutabilidad y compatibilidad total con tuplas (desempaquetado, indexación, hash). Elige dataclass cuando necesites campos mutables, fábricas de valores por defecto o métodos.
Cuándo usar namedtuple: representación de registros (filas de base de datos, líneas CSV, pares de coordenadas, colores RGB) donde importan la inmutabilidad y el bajo consumo de memoria.
deque
deque (cola de doble extremo, pronunciada "dec") es una secuencia optimizada para incorporaciones y extracciones O(1) desde ambos extremos. Una lista normal logra O(1) en append y O(n) en insert(0, …); deque logra O(1) en ambos extremos.
Creación de un deque
from collections import deque
d = deque([1, 2, 3])
print(d) # deque([1, 2, 3])Incorporar y extraer elementos
d.append(4) # add to right
d.appendleft(0) # add to left
print(d) # deque([0, 1, 2, 3, 4])
d.pop() # remove from right → 4
d.popleft() # remove from left → 0
print(d) # deque([1, 2, 3])Rotación de un deque
rotate(n) desplaza los elementos hacia la derecha n posiciones (negativo = hacia la izquierda):
d = deque([1, 2, 3])
d.rotate(1)
print(d) # deque([3, 1, 2])
d.rotate(-1)
print(d) # deque([1, 2, 3])deque acotado (ventana deslizante / búfer FIFO)
Establecer maxlen limita el tamaño del deque. Cuando se añaden nuevos elementos más allá del límite, los elementos caen automáticamente por el extremo opuesto — perfecto para conservar los últimos N eventos:
buffer = deque(maxlen=3)
for i in range(5):
buffer.append(i)
print(buffer) # deque([2, 3, 4], maxlen=3)Trampa del deque: acceso aleatorio lento O(n)
deque no admite acceso aleatorio eficiente. d[500] es O(n), no O(1) como en una lista. Si indexas frecuentemente por posición, usa una lista. Usa deque solo cuando necesites incorporaciones y extracciones rápidas en ambos extremos.
Cuándo usar deque: implementación de colas y pilas, algoritmos de ventana deslizante, búsqueda en anchura, almacenamiento de las últimas N entradas de registro.
OrderedDict
Desde Python 3.7, el dict normal mantiene el orden de inserción. ¿Por qué usar entonces OrderedDict?
Dos razones siguen siendo relevantes:
move_to_end()— permite reordenar eficientemente las claves hacia el frente o el final.- Igualdad — dos instancias de
OrderedDictcon las mismas claves pero diferente orden de inserción se comparan como no iguales, a diferencia de los dicts normales.
Creación y reordenación de un OrderedDict
from collections import OrderedDict
od = OrderedDict()
od['one'] = 1
od['two'] = 2
od['three'] = 3
print(list(od.keys())) # ['one', 'two', 'three']
od.move_to_end('one') # move 'one' to the end
print(list(od.keys())) # ['two', 'three', 'one']
od.move_to_end('three', last=False) # move 'three' to the front
print(list(od.keys())) # ['three', 'two', 'one']Igualdad sensible al orden
od1 = OrderedDict([('a', 1), ('b', 2)])
od2 = OrderedDict([('b', 2), ('a', 1)])
print(od1 == od2) # False — different order
d1 = {'a': 1, 'b': 2}
d2 = {'b': 2, 'a': 1}
print(d1 == d2) # True — regular dicts ignore orderCuándo usar OrderedDict: implementaciones de caché LRU (mover la clave usada recientemente al final), cualquier algoritmo donde el orden de inserción debe formar parte de la igualdad.
ChainMap
ChainMap agrupa múltiples diccionarios en una única vista lógica. Las búsquedas recorren los mapas en orden; las escrituras y eliminaciones afectan únicamente al primer mapa.
Uso básico
from collections import ChainMap
defaults = {'color': 'blue', 'size': 'medium', 'theme': 'light'}
overrides = {'color': 'red', 'size': 'large'}
combined = ChainMap(overrides, defaults)
print(combined['color']) # 'red' — found in overrides first
print(combined['theme']) # 'light' — not in overrides, falls back to defaultsLas escrituras van solo al primer mapa:
combined['font'] = 'serif'
print(overrides) # {'color': 'red', 'size': 'large', 'font': 'serif'}
print(defaults) # {'color': 'blue', 'size': 'medium', 'theme': 'light'} — unchangedSimulación de ámbitos de variables con new_child()
base = ChainMap({'x': 1})
child = base.new_child({'x': 99, 'y': 2})
print(child['x']) # 99 — child scope shadows parent
print(child['y']) # 2
print(child.parents['x']) # 1 — access parent scope directlynew_child() devuelve un nuevo ChainMap con un dict vacío nuevo antepuesto, que es como Python modela internamente sus propias reglas de ámbito (local → envolvente → global → incorporado).
Cuándo usar ChainMap: capas de configuración (sobreescrituras del usuario → valores por defecto del proyecto → valores por defecto globales), implementación de entornos con ámbito, combinación de argumentos de CLI con variables de entorno y archivos de configuración.
Elegir el tipo adecuado
| Necesitas… | Usa |
|---|---|
| Contar ocurrencias de elementos | Counter |
Evitar KeyError con un valor por defecto | defaultdict |
| Representar un registro con campos con nombre | namedtuple |
| Incorporaciones/extracciones rápidas en ambos extremos, o un búfer acotado | deque |
Igualdad de dict sensible al orden, o move_to_end() | OrderedDict |
| Combinar múltiples dicts en una vista sin copiar | ChainMap |
Para más información sobre los tipos base que extienden, consulta Python Dictionaries, Python Lists y Python Tuples. Para asistentes basados en iteradores de la biblioteca estándar, consulta el Módulo itertools de Python.