Módulo collections de Python: Guía de Counter, defaultdict, deque y namedtuple
Updated on
Las estructuras de datos integradas de Python —lists, dicts, tuples, sets— resuelven la mayoría de tareas. Pero cuando tu código crece más allá de ejemplos de juguete, empiezas a encontrar sus límites. Contar elementos requiere bucles manuales con diccionarios. Agrupar datos implica llenar el código con comprobaciones if key not in dict. Usar una list como cola te castiga con pops O(n) desde el frente. Representar registros estructurados con tuples simples convierte el acceso a campos en un juego ilegible de adivinar índices. Cada solución alternativa es pequeña por sí sola, pero se acumulan rápido, haciendo el código más difícil de leer, más lento de ejecutar y más propenso a fallar.
El módulo collections de la biblioteca estándar de Python resuelve estos problemas con tipos de contenedor diseñados específicamente. Counter cuenta elementos con una sola llamada. defaultdict elimina KeyError con valores por defecto automáticos. deque te da operaciones O(1) en ambos extremos de una secuencia. namedtuple añade nombres de campo a las tuples sin la sobrecarga de una clase completa. OrderedDict y ChainMap manejan patrones de orden y de búsqueda por capas que los dicts normales no expresan de forma limpia.
Esta guía cubre cada clase principal del módulo collections con código funcional, análisis de rendimiento y patrones del mundo real. Ya sea que estés procesando archivos de logs, construyendo cachés, gestionando capas de configuración o estructurando pipelines de datos, estos contenedores harán tu código más corto, más rápido y más correcto.
Visión general del módulo collections
El módulo collections proporciona tipos de datos de contenedor especializados que amplían los contenedores integrados de propósito general de Python.
import collections
# See all available classes
print([name for name in dir(collections) if not name.startswith('_')])
# ['ChainMap', 'Counter', 'OrderedDict', 'UserDict', 'UserList',
# 'UserString', 'abc', 'defaultdict', 'deque', 'namedtuple']| Class | Purpose | Replaces |
|---|---|---|
Counter | Contar objetos hashables | Bucles manuales de conteo con dict |
defaultdict | Dict con valores por defecto automáticos | dict.setdefault(), comprobaciones if key not in |
deque | Cola de doble extremo con extremos O(1) | list usada como cola/pila |
namedtuple | Tuple con campos con nombre | Tuples simples, data classes sencillas |
OrderedDict | Dict que recuerda el orden de inserción | dict (pre-3.7), operaciones ordenadas |
ChainMap | Búsquedas en diccionarios por capas | Fusión manual de dicts |
Counter: Conteo de elementos
Counter es una subclase de dict para contar objetos hashables. Mapea elementos a sus conteos y proporciona métodos para análisis de frecuencias.
Creación de un Counter
from collections import Counter
# From an iterable
words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
word_count = Counter(words)
print(word_count)
# Counter({'apple': 3, 'banana': 2, 'cherry': 1})
# From a string
letter_count = Counter('mississippi')
print(letter_count)
# Counter({'s': 4, 'i': 4, 'p': 2, 'm': 1})
# From a dictionary
inventory = Counter({'shirts': 25, 'pants': 15, 'hats': 10})
# From keyword arguments
stock = Counter(laptops=5, monitors=12)most_common() y ranking de frecuencias
from collections import Counter
text = "to be or not to be that is the question"
words = Counter(text.split())
# Get the 3 most common words
print(words.most_common(3))
# [('to', 2), ('be', 2), ('or', 1)]
# Get all elements sorted by frequency
print(words.most_common())
# [('to', 2), ('be', 2), ('or', 1), ('not', 1), ('that', 1), ('is', 1), ('the', 1), ('question', 1)]
# Least common: reverse the list or slice from the end
print(words.most_common()[-3:])
# [('is', 1), ('the', 1), ('question', 1)]Aritmética de Counter
Los Counters soportan suma, resta, intersección y unión—tratándolos como multisets.
from collections import Counter
a = Counter(x=4, y=2, z=1)
b = Counter(x=1, y=3, z=5)
# Addition: combine counts
print(a + b) # Counter({'z': 6, 'y': 5, 'x': 5})
# Subtraction: drops zero and negative results
print(a - b) # Counter({'x': 3})
# Intersection (min of each)
print(a & b) # Counter({'y': 2, 'x': 1, 'z': 1})
# Union (max of each)
print(a | b) # Counter({'z': 5, 'x': 4, 'y': 3})Patrones prácticos con Counter
from collections import Counter
# Word frequency analysis
log_entries = [
"ERROR: disk full",
"WARNING: high memory",
"ERROR: disk full",
"ERROR: timeout",
"WARNING: high memory",
"ERROR: disk full",
"INFO: backup complete",
]
error_types = Counter(entry.split(":")[0].strip() for entry in log_entries)
print(error_types)
# Counter({'ERROR': 4, 'WARNING': 2, 'INFO': 1})
# Find unique elements (count == 1)
data = [1, 2, 3, 2, 1, 4, 5, 4]
unique = [item for item, count in Counter(data).items() if count == 1]
print(unique) # [3, 5]
# Check if one collection is a subset of another (anagram check)
def is_anagram(word1, word2):
return Counter(word1.lower()) == Counter(word2.lower())
print(is_anagram("listen", "silent")) # True
print(is_anagram("hello", "world")) # FalsePara un análisis en profundidad de Counter, consulta nuestra guía dedicada de Python Counter.
defaultdict: Valores por defecto automáticos
defaultdict es una subclase de dict que llama a una función fábrica para proporcionar valores por defecto a claves ausentes, eliminando KeyError y la necesidad de comprobaciones defensivas.
Funciones fábrica
from collections import defaultdict
# int factory: default is 0
counter = defaultdict(int)
counter['apples'] += 1
counter['oranges'] += 3
print(dict(counter)) # {'apples': 1, 'oranges': 3}
# list factory: default is []
groups = defaultdict(list)
pairs = [('fruit', 'apple'), ('veggie', 'carrot'), ('fruit', 'banana'), ('veggie', 'pea')]
for category, item in pairs:
groups[category].append(item)
print(dict(groups))
# {'fruit': ['apple', 'banana'], 'veggie': ['carrot', 'pea']}
# set factory: default is set()
index = defaultdict(set)
words = [('file1', 'python'), ('file2', 'python'), ('file1', 'java'), ('file3', 'python')]
for filename, lang in words:
index[lang].add(filename)
print(dict(index))
# {'python': {'file1', 'file2', 'file3'}, 'java': {'file1'}}El patrón de agrupación
Agrupar datos relacionados es el uso más común de defaultdict(list). Compáralo con el enfoque manual:
from collections import defaultdict
students = [
('Math', 'Alice'), ('Science', 'Bob'), ('Math', 'Charlie'),
('Science', 'Diana'), ('Math', 'Eve'), ('History', 'Frank'),
]
# Without defaultdict -- verbose and error-prone
groups_manual = {}
for subject, name in students:
if subject not in groups_manual:
groups_manual[subject] = []
groups_manual[subject].append(name)
# With defaultdict -- clean and direct
groups = defaultdict(list)
for subject, name in students:
groups[subject].append(name)
print(dict(groups))
# {'Math': ['Alice', 'Charlie', 'Eve'], 'Science': ['Bob', 'Diana'], 'History': ['Frank']}defaultdict anidado
Construye estructuras de datos multinivel sin inicializar manualmente cada nivel.
from collections import defaultdict
# Two-level nested defaultdict
def nested_dict():
return defaultdict(int)
sales = defaultdict(nested_dict)
sales['2025']['Q1'] = 150000
sales['2025']['Q2'] = 175000
sales['2026']['Q1'] = 200000
print(sales['2025']['Q1']) # 150000
print(sales['2024']['Q3']) # 0 (auto-created, no KeyError)
# Arbitrary depth nesting with a recursive factory
def deep_dict():
return defaultdict(deep_dict)
config = deep_dict()
config['database']['primary']['host'] = 'localhost'
config['database']['primary']['port'] = 5432
config['database']['replica']['host'] = 'replica.local'
print(config['database']['primary']['host']) # localhostFunciones fábrica personalizadas
from collections import defaultdict
# Lambda for custom defaults
scores = defaultdict(lambda: 100) # Every student starts with 100
scores['Alice'] -= 5
scores['Bob'] -= 10
print(scores['Charlie']) # 100 (new student gets default)
print(dict(scores)) # {'Alice': 95, 'Bob': 90, 'Charlie': 100}
# Named function for complex defaults
def default_user():
return {'role': 'viewer', 'active': True, 'login_count': 0}
users = defaultdict(default_user)
users['alice']['role'] = 'admin'
print(users['bob']) # {'role': 'viewer', 'active': True, 'login_count': 0}Para más patrones, consulta nuestra guía de Python defaultdict.
deque: Cola de doble extremo
deque (se pronuncia "deck") proporciona operaciones O(1) de append y pop en ambos extremos. Las lists son O(n) para pop(0) e insert(0, x) porque todos los elementos deben desplazarse. Para cualquier carga de trabajo que toque ambos extremos de una secuencia, deque es la elección correcta.
Operaciones principales
from collections import deque
d = deque([1, 2, 3, 4, 5])
# O(1) operations on both ends
d.append(6) # Add to right: [1, 2, 3, 4, 5, 6]
d.appendleft(0) # Add to left: [0, 1, 2, 3, 4, 5, 6]
right = d.pop() # Remove from right: 6
left = d.popleft() # Remove from left: 0
print(d) # deque([1, 2, 3, 4, 5])
# Extend from both sides
d.extend([6, 7]) # Right extend: [1, 2, 3, 4, 5, 6, 7]
d.extendleft([-1, 0]) # Left extend (reversed): [0, -1, 1, 2, 3, 4, 5, 6, 7]Deques acotadas con maxlen
Cuando se establece maxlen, añadir elementos más allá del límite descarta automáticamente elementos del extremo opuesto. Esto es perfecto para ventanas deslizantes y cachés.
from collections import deque
# Keep only the last 5 items
recent = deque(maxlen=5)
for i in range(10):
recent.append(i)
print(recent) # deque([5, 6, 7, 8, 9], maxlen=5)
# Sliding window average
def moving_average(iterable, window_size):
window = deque(maxlen=window_size)
for value in iterable:
window.append(value)
if len(window) == window_size:
yield sum(window) / window_size
data = [10, 20, 30, 40, 50, 60, 70]
print(list(moving_average(data, 3)))
# [20.0, 30.0, 40.0, 50.0, 60.0]Rotación
rotate(n) desplaza elementos n pasos a la derecha. Valores negativos rotan a la izquierda.
from collections import deque
d = deque([1, 2, 3, 4, 5])
d.rotate(2) # Rotate right by 2
print(d) # deque([4, 5, 1, 2, 3])
d.rotate(-3) # Rotate left by 3
print(d) # deque([2, 3, 4, 5, 1])Rendimiento: deque vs list
from collections import deque
import time
# Benchmark: append/pop from left side
n = 100_000
# List: O(n) for each insert at position 0
start = time.perf_counter()
lst = []
for i in range(n):
lst.insert(0, i)
list_time = time.perf_counter() - start
# Deque: O(1) for appendleft
start = time.perf_counter()
dq = deque()
for i in range(n):
dq.appendleft(i)
deque_time = time.perf_counter() - start
print(f"List insert(0, x): {list_time:.4f}s")
print(f"Deque appendleft: {deque_time:.4f}s")
print(f"Deque is {list_time / deque_time:.0f}x faster")
# Typical output:
# List insert(0, x): 1.2340s
# Deque appendleft: 0.0065s
# Deque is 190x faster| Operation | list | deque |
|---|---|---|
append(x) (right) | O(1) amortized | O(1) |
pop() (right) | O(1) | O(1) |
insert(0, x) / appendleft(x) | O(n) | O(1) |
pop(0) / popleft() | O(n) | O(1) |
access by index [i] | O(1) | O(n) |
| Memory per element | Menor | Ligeramente mayor |
Usa deque cuando necesites operaciones rápidas en ambos extremos. Usa list cuando necesites acceso aleatorio rápido por índice.
Para la guía completa, consulta Python deque.
namedtuple: Tuples con campos con nombre
namedtuple crea subclases de tuple con campos con nombre, haciendo que el código sea autoexplicativo sin la sobrecarga de definir una clase completa.
Creación de namedtuples
from collections import namedtuple
# Define a type
Point = namedtuple('Point', ['x', 'y'])
p = Point(3, 4)
# Access by name or index
print(p.x) # 3
print(p[1]) # 4
print(p) # Point(x=3, y=4)
# Alternative field definition styles
Color = namedtuple('Color', 'red green blue') # Space-separated string
Config = namedtuple('Config', 'host, port, database') # Comma-separated string¿Por qué usar namedtuple en lugar de tuples simples?
from collections import namedtuple
# Plain tuple: which index is what?
employee_tuple = ('Alice', 'Engineering', 95000, True)
print(employee_tuple[2]) # 95000 -- but what does index 2 mean?
# namedtuple: self-documenting
Employee = namedtuple('Employee', 'name department salary active')
employee = Employee('Alice', 'Engineering', 95000, True)
print(employee.salary) # 95000 -- immediately clear
print(employee.department) # EngineeringMétodos clave
from collections import namedtuple
Employee = namedtuple('Employee', 'name department salary')
emp = Employee('Alice', 'Engineering', 95000)
# _replace: create a new instance with some fields changed (immutable)
promoted = emp._replace(salary=110000)
print(promoted) # Employee(name='Alice', department='Engineering', salary=110000)
print(emp) # Employee(name='Alice', department='Engineering', salary=95000) -- unchanged
# _asdict: convert to OrderedDict (Python 3.8+ returns regular dict)
print(emp._asdict())
# {'name': 'Alice', 'department': 'Engineering', 'salary': 95000}
# _fields: get field names
print(Employee._fields) # ('name', 'department', 'salary')
# _make: create from an iterable
data = ['Bob', 'Marketing', 85000]
emp2 = Employee._make(data)
print(emp2) # Employee(name='Bob', department='Marketing', salary=85000)Valores por defecto
from collections import namedtuple
# defaults parameter (Python 3.6.1+)
Connection = namedtuple('Connection', 'host port timeout', defaults=[5432, 30])
conn1 = Connection('localhost') # port=5432, timeout=30
conn2 = Connection('db.example.com', 3306) # timeout=30
conn3 = Connection('db.example.com', 3306, 60)
print(conn1) # Connection(host='localhost', port=5432, timeout=30)
print(conn2) # Connection(host='db.example.com', port=3306, timeout=30)Alternativa: typing.NamedTuple
Para anotaciones de tipos y una sintaxis más parecida a clase, usa typing.NamedTuple:
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
label: str = "origin"
p = Point(3.0, 4.0, "A")
print(p.x, p.label) # 3.0 A
# Still a tuple -- supports unpacking, indexing, iteration
x, y, label = p
print(f"({x}, {y})") # (3.0, 4.0)namedtuple vs dataclass
| Feature | namedtuple | dataclass |
|---|---|---|
| Inmutable por defecto | Sí | No (frozen=True requerido) |
| Huella de memoria | Igual que tuple (pequeña) | Mayor (clase normal) |
| Iteración/desempaquetado | Sí (es una tuple) | No (a menos que añadas métodos) |
| Anotaciones de tipo | Vía typing.NamedTuple | Integradas |
| Métodos/propiedades | Requiere subclassing | Soporte directo |
| Herencia | Limitada | Herencia completa de clases |
| Mejor para | Registros de datos ligeros | Objetos complejos mutables |
OrderedDict: Operaciones de diccionario ordenado
Desde Python 3.7, el dict normal preserva el orden de inserción. Entonces, ¿cuándo necesitas todavía OrderedDict?
Cuándo OrderedDict sigue importando
from collections import OrderedDict
# 1. Equality considers order
d1 = {'a': 1, 'b': 2}
d2 = {'b': 2, 'a': 1}
print(d1 == d2) # True -- regular dicts ignore order in comparison
od1 = OrderedDict([('a', 1), ('b', 2)])
od2 = OrderedDict([('b', 2), ('a', 1)])
print(od1 == od2) # False -- OrderedDict considers order
# 2. move_to_end() for reordering
od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
od.move_to_end('a') # Move 'a' to the end
print(list(od.keys())) # ['b', 'c', 'a']
od.move_to_end('c', last=False) # Move 'c' to the beginning
print(list(od.keys())) # ['c', 'b', 'a']Construir una caché LRU con OrderedDict
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.cache = OrderedDict()
self.capacity = capacity
def get(self, key):
if key not in self.cache:
return -1
self.cache.move_to_end(key) # Mark as recently used
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False) # Remove oldest
cache = LRUCache(3)
cache.put('a', 1)
cache.put('b', 2)
cache.put('c', 3)
cache.get('a') # Access 'a', moves it to end
cache.put('d', 4) # Evicts 'b' (least recently used)
print(list(cache.cache.keys())) # ['c', 'a', 'd']ChainMap: Búsquedas en diccionarios por capas
ChainMap agrupa múltiples diccionarios en una sola vista para búsquedas. Busca en cada diccionario en orden, devolviendo la primera coincidencia. Esto es ideal para capas de configuración, búsquedas de variables con scope y gestión de contexto.
Uso básico
from collections import ChainMap
defaults = {'theme': 'light', 'language': 'en', 'timeout': 30}
user_prefs = {'theme': 'dark'}
session = {'language': 'fr'}
config = ChainMap(session, user_prefs, defaults)
# Lookup searches session -> user_prefs -> defaults
print(config['theme']) # 'dark' (from user_prefs)
print(config['language']) # 'fr' (from session)
print(config['timeout']) # 30 (from defaults)Capas de configuración
from collections import ChainMap
import os
# Real-world config pattern: CLI args > env vars > config file > defaults
defaults = {
'debug': False,
'log_level': 'WARNING',
'port': 8080,
'host': '0.0.0.0',
}
config_file = {
'log_level': 'INFO',
'port': 9090,
}
env_vars = {
k.lower(): v for k, v in os.environ.items()
if k.lower() in defaults
}
cli_args = {'debug': True} # Parsed from argparse
config = ChainMap(cli_args, env_vars, config_file, defaults)
print(config['debug']) # True (from cli_args)
print(config['log_level']) # 'INFO' (from config_file)
print(config['host']) # '0.0.0.0' (from defaults)Contextos con scope usando new_child()
from collections import ChainMap
# Simulating variable scoping (like nested function scopes)
global_scope = {'x': 1, 'y': 2}
local_scope = ChainMap(global_scope)
# Enter a new scope
inner_scope = local_scope.new_child()
inner_scope['x'] = 10 # Shadows global x
inner_scope['z'] = 30 # New local variable
print(inner_scope['x']) # 10 (local)
print(inner_scope['y']) # 2 (falls through to global)
print(inner_scope['z']) # 30 (local)
# Exit scope -- original is unchanged
print(local_scope['x']) # 1 (global still intact)Comparación de todos los tipos de collections
| Type | Base Class | Mutable | Use Case | Key Advantage |
|---|---|---|---|---|
Counter | dict | Sí | Conteo de elementos | most_common(), aritmética de multiset |
defaultdict | dict | Sí | Auto-inicializar claves ausentes | Sin KeyError, funciones fábrica |
deque | -- | Sí | Cola de doble extremo | O(1) en ambos extremos, maxlen |
namedtuple | tuple | No | Registros de datos estructurados | Acceso por campos con nombre, ligero |
OrderedDict | dict | Sí | Dicts sensibles al orden | move_to_end(), igualdad con orden |
ChainMap | -- | Sí | Búsquedas por capas | Capas de configuración, scopes |
Benchmarks de rendimiento
Counter vs conteo manual
from collections import Counter, defaultdict
import time
data = list(range(1000)) * 1000 # 1 million items, 1000 unique
# Method 1: Counter
start = time.perf_counter()
c = Counter(data)
counter_time = time.perf_counter() - start
# Method 2: defaultdict(int)
start = time.perf_counter()
dd = defaultdict(int)
for item in data:
dd[item] += 1
dd_time = time.perf_counter() - start
# Method 3: Manual dict
start = time.perf_counter()
manual = {}
for item in data:
manual[item] = manual.get(item, 0) + 1
manual_time = time.perf_counter() - start
print(f"Counter: {counter_time:.4f}s")
print(f"defaultdict(int):{dd_time:.4f}s")
print(f"dict.get(): {manual_time:.4f}s")
# Typical: Counter ~0.03s, defaultdict ~0.07s, dict.get() ~0.09sdeque vs list para operaciones de cola
from collections import deque
import time
n = 100_000
# Simulate a FIFO queue: append right, pop left
# List
start = time.perf_counter()
q = list(range(n))
while q:
q.pop(0)
list_queue_time = time.perf_counter() - start
# Deque
start = time.perf_counter()
q = deque(range(n))
while q:
q.popleft()
deque_queue_time = time.perf_counter() - start
print(f"List pop(0): {list_queue_time:.4f}s")
print(f"Deque popleft(): {deque_queue_time:.4f}s")
print(f"Deque is {list_queue_time / deque_queue_time:.0f}x faster")
# Typical: List ~2.5s, Deque ~0.004s -> ~600x fasterEjemplos del mundo real
Análisis de logs con Counter
from collections import Counter
from datetime import datetime
# Parse and analyze server logs
log_lines = [
"2026-02-18 10:15:03 GET /api/users 200",
"2026-02-18 10:15:04 POST /api/login 401",
"2026-02-18 10:15:05 GET /api/users 200",
"2026-02-18 10:15:06 GET /api/products 500",
"2026-02-18 10:15:07 POST /api/login 200",
"2026-02-18 10:15:08 GET /api/users 200",
"2026-02-18 10:15:09 GET /api/products 500",
"2026-02-18 10:15:10 POST /api/login 401",
]
# Count status codes
status_codes = Counter(line.split()[-1] for line in log_lines)
print("Status codes:", status_codes.most_common())
# [('200', 4), ('401', 2), ('500', 2)]
# Count endpoints
endpoints = Counter(line.split()[3] for line in log_lines)
print("Top endpoints:", endpoints.most_common(2))
# [('/api/users', 3), ('/api/login', 3)]
# Count error endpoints (status >= 400)
errors = Counter(
line.split()[3] for line in log_lines
if int(line.split()[-1]) >= 400
)
print("Error endpoints:", errors)
# Counter({'/api/login': 2, '/api/products': 2})Gestión de configuración con ChainMap
from collections import ChainMap
import json
# Multi-layer config system for a web application
def load_config(config_path=None, cli_overrides=None):
# Layer 1: Hard-coded defaults
defaults = {
'host': '127.0.0.1',
'port': 8000,
'debug': False,
'db_pool_size': 5,
'log_level': 'WARNING',
'cors_origins': ['http://localhost:3000'],
}
# Layer 2: Config file
file_config = {}
if config_path:
with open(config_path) as f:
file_config = json.load(f)
# Layer 3: CLI overrides (highest priority)
cli = cli_overrides or {}
# ChainMap searches cli -> file_config -> defaults
return ChainMap(cli, file_config, defaults)
# Usage
config = load_config(cli_overrides={'debug': True, 'port': 9000})
print(config['debug']) # True (CLI override)
print(config['port']) # 9000 (CLI override)
print(config['db_pool_size']) # 5 (default)
print(config['log_level']) # WARNING (default)Caché de elementos recientes con deque
from collections import deque
class RecentItemsTracker:
"""Track the N most recent unique items."""
def __init__(self, max_items=10):
self.items = deque(maxlen=max_items)
self.seen = set()
def add(self, item):
if item in self.seen:
# Move to front by removing and re-adding
self.items.remove(item)
self.items.append(item)
else:
if len(self.items) == self.items.maxlen:
# Remove the oldest item from the set too
oldest = self.items[0]
self.seen.discard(oldest)
self.items.append(item)
self.seen.add(item)
def get_recent(self):
return list(reversed(self.items))
# Track recently viewed products
tracker = RecentItemsTracker(max_items=5)
for product in ['shoes', 'shirt', 'hat', 'shoes', 'jacket', 'belt', 'hat']:
tracker.add(product)
print(tracker.get_recent())
# ['hat', 'belt', 'jacket', 'shoes', 'shirt']Pipeline de datos con namedtuple
from collections import namedtuple, Counter, defaultdict
# Define structured records
Transaction = namedtuple('Transaction', 'id customer product amount date')
transactions = [
Transaction(1, 'Alice', 'Widget', 29.99, '2026-02-01'),
Transaction(2, 'Bob', 'Gadget', 49.99, '2026-02-01'),
Transaction(3, 'Alice', 'Widget', 29.99, '2026-02-03'),
Transaction(4, 'Charlie', 'Gadget', 49.99, '2026-02-05'),
Transaction(5, 'Alice', 'Gizmo', 19.99, '2026-02-07'),
Transaction(6, 'Bob', 'Widget', 29.99, '2026-02-08'),
]
# Most popular products
product_count = Counter(t.product for t in transactions)
print("Popular products:", product_count.most_common())
# [('Widget', 3), ('Gadget', 2), ('Gizmo', 1)]
# Revenue by customer
revenue = defaultdict(float)
for t in transactions:
revenue[t.customer] += t.amount
print("Revenue:", dict(revenue))
# {'Alice': 79.97, 'Bob': 79.98, 'Charlie': 49.99}
# Convert to DataFrame for visualization
import pandas as pd
df = pd.DataFrame(transactions, columns=Transaction._fields)
print(df.groupby('customer')['amount'].sum())Visualización de datos de collections con PyGWalker
Después de procesar datos con Counter, defaultdict o namedtuple, a menudo quieres visualizar los resultados. PyGWalker (opens in a new tab) convierte cualquier pandas DataFrame en una interfaz de visualización interactiva estilo Tableau directamente en Jupyter notebooks:
from collections import Counter
import pandas as pd
import pygwalker as pyg
# Process data with collections
log_data = ["ERROR", "WARNING", "ERROR", "INFO", "ERROR", "WARNING", "INFO", "INFO"]
counts = Counter(log_data)
# Convert to DataFrame
df = pd.DataFrame(counts.items(), columns=['Level', 'Count'])
# Launch interactive visualization
walker = pyg.walk(df)Esto te permite arrastrar y soltar campos, crear gráficos, filtrar datos y explorar patrones de forma interactiva, sin escribir código de visualización. Es especialmente útil cuando tienes datasets grandes procesados mediante agrupaciones con Counter o defaultdict y quieres explorar visualmente las distribuciones.
Para ejecutar estos experimentos de collections de forma interactiva, RunCell (opens in a new tab) ofrece un entorno Jupyter impulsado por AI donde puedes iterar sobre pipelines de procesamiento de datos con feedback instantáneo.
Combinación de múltiples tipos de collections
El verdadero poder de collections aparece cuando combinas tipos en un solo pipeline.
from collections import Counter, defaultdict, namedtuple, deque
# Named record type
LogEntry = namedtuple('LogEntry', 'timestamp level message')
# Simulated log stream
log_stream = deque([
LogEntry('10:01', 'ERROR', 'Connection timeout'),
LogEntry('10:02', 'INFO', 'Request processed'),
LogEntry('10:03', 'ERROR', 'Connection timeout'),
LogEntry('10:04', 'WARNING', 'High memory'),
LogEntry('10:05', 'ERROR', 'Disk full'),
LogEntry('10:06', 'INFO', 'Request processed'),
LogEntry('10:07', 'ERROR', 'Connection timeout'),
], maxlen=100)
# Count error types
error_counts = Counter(
entry.message for entry in log_stream if entry.level == 'ERROR'
)
print("Error types:", error_counts.most_common())
# [('Connection timeout', 3), ('Disk full', 1)]
# Group entries by level
by_level = defaultdict(list)
for entry in log_stream:
by_level[entry.level].append(entry)
for level, entries in by_level.items():
print(f"{level}: {len(entries)} entries")
# ERROR: 4 entries
# INFO: 2 entries
# WARNING: 1 entriesPreguntas frecuentes (FAQ)
¿Qué es el módulo collections de Python?
El módulo collections forma parte de la biblioteca estándar de Python. Proporciona tipos de datos de contenedor especializados que amplían los tipos integrados (dict, list, tuple, set) con funcionalidad adicional. Las clases principales son Counter, defaultdict, deque, namedtuple, OrderedDict y ChainMap. Cada una resuelve una categoría específica de problemas de manejo de datos de forma más eficiente que los tipos integrados por sí solos.
¿Cuándo debería usar Counter vs defaultdict(int)?
Usa Counter cuando tu objetivo principal sea contar elementos o comparar distribuciones de frecuencia. Proporciona most_common(), operadores aritméticos (+, -, &, |) y puede contar un iterable completo con una sola llamada al constructor. Usa defaultdict(int) cuando el conteo sea incidental dentro de un patrón de estructura de datos más amplio, o cuando necesites un diccionario de propósito general con valores enteros por defecto.
¿deque es thread-safe en Python?
Sí. En CPython, deque.append(), deque.appendleft(), deque.pop() y deque.popleft() son operaciones atómicas debido al GIL (Global Interpreter Lock). Esto hace que deque sea seguro para usarlo como una cola thread-safe sin locking adicional. Sin embargo, las operaciones compuestas (como secuencias de “comprobar y luego actuar”) aún necesitan sincronización explícita.
¿Cuál es la diferencia entre namedtuple y dataclass?
namedtuple crea subclases inmutables de tuple con campos con nombre. Es ligero, soporta iteración y desempaquetado, y usa memoria mínima. dataclass (del módulo dataclasses, Python 3.7+) crea clases completas con atributos mutables por defecto, y soporta métodos, propiedades y herencia. Usa namedtuple para registros de datos simples e inmutables. Usa dataclass cuando necesites mutabilidad, comportamiento complejo o anotaciones de tipo extensas.
¿OrderedDict todavía importa en Python 3.7+?
Sí, en dos casos específicos. Primero, las comparaciones de igualdad de OrderedDict consideran el orden de los elementos (OrderedDict(a=1, b=2) != OrderedDict(b=2, a=1)), mientras que los dicts normales no. Segundo, OrderedDict proporciona move_to_end() para reordenar elementos, lo cual es útil para implementar cachés LRU y estructuras de datos basadas en prioridad. Para el resto de casos, un dict normal es suficiente y más eficiente.
¿En qué se diferencia ChainMap de fusionar diccionarios?
ChainMap crea una vista sobre múltiples diccionarios sin copiar los datos. Las búsquedas recorren cada diccionario en orden. Los cambios en los diccionarios subyacentes se reflejan inmediatamente en el ChainMap. En cambio, fusionar con {**d1, **d2} o d1 | d2 crea un diccionario nuevo, duplicando todos los datos. ChainMap es más eficiente en memoria para diccionarios grandes y preserva la estructura por capas para patrones de configuración y scoping.
¿Puedo usar tipos de collections con type hints?
Sí. Usa collections.Counter[str] para counters tipados, collections.defaultdict[str, list[int]] para defaultdicts tipados, y collections.deque[int] para deques tipados. Para namedtuple, prefiere typing.NamedTuple, que soporta anotaciones de tipos directamente en la definición de la clase. Todos los tipos de collections son totalmente compatibles con mypy y otros type checkers.
Conclusión
El módulo collections de Python proporciona seis tipos de contenedor especializados que eliminan patrones comunes de boilerplate. Counter reemplaza los bucles manuales de conteo. defaultdict elimina el manejo de KeyError. deque te ofrece operaciones rápidas de doble extremo. namedtuple añade nombres de campo legibles a las tuples. OrderedDict maneja comparaciones sensibles al orden y reordenamiento. ChainMap gestiona búsquedas en diccionarios por capas sin duplicación de datos.
Cada tipo resuelve un problema específico mejor que los contenedores integrados. Aprender cuándo usar cada uno hará tu código Python más corto, más rápido y más fácil de mantener. La clave es hacer coincidir la estructura de datos con el patrón de operaciones: conteo (Counter), agrupación (defaultdict), cola/pila (deque), registros estructurados (namedtuple), operaciones ordenadas (OrderedDict) y búsquedas por capas (ChainMap).