Python
Tutorial Python: Módulo collections
Aprende a usar el módulo collections de Python con namedtuple, defaultdict, Counter y deque para estructuras de datos eficientes y legibles.
Aprende Python y certifícatenamedtuple
Las tuplas en Python son estructuras de datos inmutables que permiten almacenar colecciones ordenadas de elementos. Sin embargo, cuando trabajamos con tuplas que representan entidades con múltiples atributos, acceder a los elementos mediante índices numéricos puede resultar poco intuitivo y propenso a errores. Aquí es donde entra en juego namedtuple
del módulo collections
.
namedtuple
es una factoría de clases que permite crear subclases de tuplas con campos nombrados. Esto combina la eficiencia y características de las tuplas inmutables con la legibilidad y conveniencia del acceso a atributos por nombre.
Sintaxis básica
Para crear una namedtuple, necesitamos importarla del módulo collections y definir su estructura:
from collections import namedtuple
# Sintaxis: namedtuple(nombre_clase, campos)
Punto = namedtuple('Punto', ['x', 'y'])
# También se puede definir con una cadena separada por espacios
Persona = namedtuple('Persona', 'nombre edad profesion')
Creación y uso de namedtuples
Una vez definida la estructura, podemos crear instancias y acceder a sus campos de múltiples formas:
# Creación de instancias
p = Punto(10, 20)
ana = Persona('Ana', 28, 'Ingeniera')
# Acceso por nombre de campo
print(p.x) # 10
print(ana.nombre) # Ana
# Acceso por índice (como tuplas normales)
print(p[0]) # 10
print(ana[1]) # 28
# Desempaquetado
x, y = p
nombre, edad, profesion = ana
Ventajas sobre tuplas regulares
Para ilustrar las ventajas de namedtuple, comparemos el mismo código usando tuplas regulares y namedtuples:
# Con tuplas regulares
punto_tupla = (10, 20)
# ¿Qué representa cada valor?
print(f"Coordenada X: {punto_tupla[0]}, Coordenada Y: {punto_tupla[1]}")
# Con namedtuples
punto_named = Punto(10, 20)
# Mucho más legible y autoexplicativo
print(f"Coordenada X: {punto_named.x}, Coordenada Y: {punto_named.y}")
Métodos y atributos especiales
Las namedtuples heredan todos los métodos de las tuplas regulares y añaden algunos propios:
# Convertir a diccionario
punto_dict = p._asdict()
print(punto_dict) # {'x': 10, 'y': 20}
# Crear una copia con campos modificados
p2 = p._replace(x=15)
print(p2) # Punto(x=15, y=20)
# Ver los nombres de los campos
print(p._fields) # ('x', 'y')
# Crear una namedtuple a partir de un iterable
valores = [30, 40]
p3 = Punto._make(valores)
print(p3) # Punto(x=30, y=40)
Casos de uso prácticos
Las namedtuples son especialmente útiles en situaciones donde necesitamos estructuras de datos ligeras e inmutables con campos nombrados:
1. Representación de registros de datos
Registro = namedtuple('Registro', 'timestamp valor usuario')
lecturas = [
Registro('2023-10-15 10:30', 23.5, 'admin'),
Registro('2023-10-15 10:35', 24.1, 'sensor1'),
Registro('2023-10-15 10:40', 23.8, 'sensor2')
]
# Fácil de procesar y entender
for lectura in lecturas:
print(f"A las {lectura.timestamp}, {lectura.usuario} registró {lectura.valor}")
2. Retorno de múltiples valores en funciones
def obtener_estadisticas(numeros):
Estadisticas = namedtuple('Estadisticas', 'media mediana maximo minimo')
return Estadisticas(
sum(numeros) / len(numeros),
sorted(numeros)[len(numeros) // 2],
max(numeros),
min(numeros)
)
datos = [4, 7, 1, 9, 3, 6, 8]
stats = obtener_estadisticas(datos)
# Acceso claro a los resultados
print(f"Media: {stats.media:.2f}")
print(f"Mediana: {stats.mediana}")
print(f"Rango: {stats.minimo} - {stats.maximo}")
3. Integración con otras bibliotecas
Las namedtuples se integran perfectamente con otras bibliotecas como pandas:
import pandas as pd
from collections import namedtuple
Producto = namedtuple('Producto', 'id nombre precio stock')
productos = [
Producto(1, 'Teclado', 45.99, 23),
Producto(2, 'Monitor', 199.50, 15),
Producto(3, 'Ratón', 25.75, 48)
]
# Convertir a DataFrame de pandas
df = pd.DataFrame(productos)
print(df)
Consideraciones de rendimiento
Las namedtuples son casi tan eficientes como las tuplas regulares en términos de memoria y velocidad:
import sys
import timeit
# Comparación de memoria
punto_tupla = (10, 20)
punto_named = Punto(10, 20)
print(f"Tamaño tupla: {sys.getsizeof(punto_tupla)} bytes")
print(f"Tamaño namedtuple: {sys.getsizeof(punto_named)} bytes")
# Comparación de velocidad de acceso
def acceso_tupla():
return punto_tupla[0]
def acceso_named():
return punto_named.x
t1 = timeit.timeit(acceso_tupla, number=1000000)
t2 = timeit.timeit(acceso_named, number=1000000)
print(f"Tiempo acceso tupla: {t1:.6f} segundos")
print(f"Tiempo acceso namedtuple: {t2:.6f} segundos")
Limitaciones
Aunque las namedtuples son muy útiles, tienen algunas limitaciones:
- Son inmutables, por lo que no se pueden modificar después de creadas (aunque esto puede ser una ventaja en muchos casos).
- Los nombres de los campos deben ser identificadores válidos de Python.
- No permiten valores por defecto (para eso existen otras estructuras como
dataclasses
).
Creación dinámica de namedtuples
En algunos casos, necesitamos crear namedtuples dinámicamente basadas en datos externos:
def crear_namedtuple_desde_csv(archivo):
import csv
with open(archivo, 'r') as f:
reader = csv.reader(f)
campos = next(reader) # Primera fila como nombres de campos
RegistroCSV = namedtuple('RegistroCSV', campos)
registros = []
for fila in reader:
registros.append(RegistroCSV(*fila))
return registros
# Uso
# registros = crear_namedtuple_desde_csv('datos.csv')
Las namedtuples representan un equilibrio perfecto entre la simplicidad de las tuplas y la expresividad de las clases, proporcionando una forma elegante y eficiente de trabajar con datos estructurados inmutables en Python.
defaultdict
El manejo de diccionarios en Python es una tarea común, pero a veces nos encontramos con situaciones donde necesitamos inicializar valores automáticamente o manejar claves inexistentes de forma elegante. La clase defaultdict
del módulo collections
resuelve precisamente este problema, proporcionando una versión especializada de diccionarios con valores predeterminados.
Un defaultdict
es un diccionario con valores por defecto que se generan automáticamente cuando intentamos acceder a una clave que no existe. Esto elimina la necesidad de verificar constantemente si una clave existe antes de usarla, simplificando nuestro código y haciéndolo más legible.
Sintaxis básica
Para utilizar defaultdict
, primero debemos importarlo del módulo collections:
from collections import defaultdict
# Creamos un defaultdict con valores por defecto de tipo int
contador = defaultdict(int)
# Creamos un defaultdict con valores por defecto de tipo list
agrupador = defaultdict(list)
# Creamos un defaultdict con valores por defecto de tipo str
cadenas = defaultdict(str)
El argumento que pasamos al constructor de defaultdict
es una función factoría que será llamada sin argumentos para proporcionar el valor por defecto cuando se acceda a una clave inexistente.
Comparación con diccionarios estándar
Para entender mejor las ventajas de defaultdict
, comparemos cómo manejaríamos un contador de palabras con un diccionario estándar y con un defaultdict
:
# Con un diccionario estándar
texto = "el perro persigue al gato y el gato corre"
palabras = texto.split()
# Enfoque con diccionario estándar
contador_dict = {}
for palabra in palabras:
if palabra in contador_dict:
contador_dict[palabra] += 1
else:
contador_dict[palabra] = 1
print(contador_dict) # {'el': 2, 'perro': 1, 'persigue': 1, 'al': 1, 'gato': 2, 'y': 1, 'corre': 1}
# Enfoque con defaultdict
contador_default = defaultdict(int)
for palabra in palabras:
contador_default[palabra] += 1
print(dict(contador_default)) # {'el': 2, 'perro': 1, 'persigue': 1, 'al': 1, 'gato': 2, 'y': 1, 'corre': 1}
Como podemos ver, con defaultdict
eliminamos la verificación condicional, haciendo el código más conciso y legible.
Tipos de valores por defecto comunes
Podemos usar diferentes tipos de factorías para crear valores por defecto según nuestras necesidades:
# Valores numéricos inicializados en 0
contadores = defaultdict(int)
contadores['manzanas'] += 3
contadores['naranjas'] += 2
print(contadores) # defaultdict(<class 'int'>, {'manzanas': 3, 'naranjas': 2})
# Listas vacías
grupos = defaultdict(list)
grupos['frutas'].append('manzana')
grupos['frutas'].append('naranja')
grupos['verduras'].append('zanahoria')
print(grupos) # defaultdict(<class 'list'>, {'frutas': ['manzana', 'naranja'], 'verduras': ['zanahoria']})
# Conjuntos vacíos
tags = defaultdict(set)
tags['python'].add('lenguaje')
tags['python'].add('programación')
tags['python'].add('lenguaje') # Los duplicados se eliminan automáticamente
print(tags) # defaultdict(<class 'set'>, {'python': {'lenguaje', 'programación'}})
Funciones factoría personalizadas
Además de los tipos integrados, podemos definir nuestras propias funciones factoría:
def valor_por_defecto():
return "No disponible"
info = defaultdict(valor_por_defecto)
info['nombre'] = 'Ana'
print(info['nombre']) # Ana
print(info['dirección']) # No disponible
# También podemos usar lambdas
precios = defaultdict(lambda: 0.0)
precios['manzana'] = 1.25
print(precios['manzana']) # 1.25
print(precios['pera']) # 0.0
Casos de uso prácticos
1. Agrupación de datos
Uno de los usos más comunes de defaultdict
es agrupar datos por alguna clave:
# Agrupar estudiantes por curso
estudiantes = [
('Matemáticas', 'Ana'),
('Física', 'Carlos'),
('Matemáticas', 'Elena'),
('Historia', 'David'),
('Física', 'Beatriz')
]
cursos = defaultdict(list)
for curso, estudiante in estudiantes:
cursos[curso].append(estudiante)
# Resultado: {'Matemáticas': ['Ana', 'Elena'], 'Física': ['Carlos', 'Beatriz'], 'Historia': ['David']}
for curso, alumnos in cursos.items():
print(f"{curso}: {', '.join(alumnos)}")
2. Conteo y estadísticas
defaultdict
es ideal para tareas de conteo y estadísticas:
# Análisis de frecuencia de caracteres
texto = "Python es un lenguaje de programación interpretado"
frecuencia = defaultdict(int)
for caracter in texto.lower():
frecuencia[caracter] += 1
# Mostrar los 5 caracteres más frecuentes
caracteres_frecuentes = sorted(frecuencia.items(), key=lambda x: x[1], reverse=True)[:5]
for caracter, conteo in caracteres_frecuentes:
print(f"'{caracter}': {conteo}")
3. Caché con valores calculados
Podemos usar defaultdict
para implementar un caché simple:
import time
# Función costosa que queremos cachear
def calcular_factorial(n):
time.sleep(0.1) # Simulamos una operación costosa
if n <= 1:
return 1
return n * calcular_factorial(n-1)
# Caché usando defaultdict
cache_factorial = defaultdict(lambda: None)
def factorial_con_cache(n):
if cache_factorial[n] is None:
print(f"Calculando factorial de {n}...")
cache_factorial[n] = calcular_factorial(n)
return cache_factorial[n]
# Primera llamada (calcula y guarda en caché)
print(factorial_con_cache(5)) # Calculando factorial de 5...
# Segunda llamada (usa el valor cacheado)
print(factorial_con_cache(5)) # Usa el valor en caché
4. Construcción de estructuras de datos anidadas
defaultdict
facilita la creación de estructuras de datos anidadas:
# Diccionario anidado para representar un árbol de categorías
categorias = defaultdict(lambda: defaultdict(list))
# Añadir productos a categorías y subcategorías
categorias['Electrónica']['Smartphones'].append('iPhone 13')
categorias['Electrónica']['Smartphones'].append('Samsung Galaxy')
categorias['Electrónica']['Portátiles'].append('MacBook Pro')
categorias['Hogar']['Cocina'].append('Batidora')
# Acceso a datos anidados sin preocuparnos por claves inexistentes
categorias['Ropa']['Camisetas'].append('Camiseta básica')
# Recorrer la estructura
for categoria, subcategorias in categorias.items():
print(f"Categoría: {categoria}")
for subcategoria, productos in subcategorias.items():
print(f" Subcategoría: {subcategoria}")
for producto in productos:
print(f" - {producto}")
Rendimiento y consideraciones
El uso de defaultdict
no solo mejora la legibilidad del código, sino que también puede tener ventajas de rendimiento al eliminar comprobaciones repetitivas:
import timeit
# Comparación de rendimiento entre dict y defaultdict para conteo
setup = """
texto = "Python es un lenguaje de programación interpretado cuya filosofía hace hincapié en la legibilidad de su código" * 100
palabras = texto.split()
from collections import defaultdict
"""
codigo_dict = """
contador = {}
for palabra in palabras:
if palabra in contador:
contador[palabra] += 1
else:
contador[palabra] = 1
"""
codigo_defaultdict = """
contador = defaultdict(int)
for palabra in palabras:
contador[palabra] += 1
"""
tiempo_dict = timeit.timeit(codigo_dict, setup=setup, number=1000)
tiempo_defaultdict = timeit.timeit(codigo_defaultdict, setup=setup, number=1000)
print(f"Tiempo con dict: {tiempo_dict:.6f} segundos")
print(f"Tiempo con defaultdict: {tiempo_defaultdict:.6f} segundos")
print(f"Mejora: {(tiempo_dict - tiempo_defaultdict) / tiempo_dict * 100:.2f}%")
Integración con otras estructuras de collections
defaultdict
se integra perfectamente con otras estructuras del módulo collections
:
from collections import defaultdict, Counter
# Combinación de defaultdict y Counter para análisis de texto por párrafos
texto = """
Párrafo 1: Python es un lenguaje de programación interpretado.
Párrafo 2: Python es multiparadigma y de tipado dinámico.
Párrafo 3: Python fue creado por Guido van Rossum.
"""
# Analizamos palabras por párrafo
analisis = defaultdict(Counter)
for i, parrafo in enumerate(texto.strip().split('\n'), 1):
palabras = parrafo.split(':')[1].lower().split()
analisis[f"Párrafo {i}"].update(palabras)
# Mostramos las 3 palabras más comunes por párrafo
for parrafo, contador in analisis.items():
print(f"\n{parrafo}:")
for palabra, frecuencia in contador.most_common(3):
print(f" {palabra}: {frecuencia}")
El defaultdict
es una herramienta versátil que simplifica el manejo de diccionarios en Python, especialmente cuando necesitamos inicializar valores automáticamente. Su uso adecuado puede hacer que nuestro código sea más limpio, más eficiente y menos propenso a errores relacionados con claves inexistentes.
Counter
El módulo collections
de Python ofrece varias estructuras de datos especializadas que extienden las capacidades de las estructuras básicas. Entre ellas, Counter
destaca como una herramienta eficiente para contar elementos y realizar operaciones relacionadas con frecuencias.
Counter
es una subclase de diccionario diseñada específicamente para contar objetos hashables. Funciona como un diccionario donde las claves son los elementos a contar y los valores representan sus frecuencias o conteos. A diferencia de un diccionario normal, Counter
proporciona métodos especializados para tareas de conteo y análisis de frecuencias.
Creación de un Counter
Existen varias formas de crear un objeto Counter:
from collections import Counter
# A partir de una secuencia de elementos
c1 = Counter(['a', 'b', 'c', 'a', 'b', 'a'])
print(c1) # Counter({'a': 3, 'b': 2, 'c': 1})
# A partir de una cadena
c2 = Counter("abracadabra")
print(c2) # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
# A partir de pares clave-valor
c3 = Counter(a=3, b=2, c=1)
print(c3) # Counter({'a': 3, 'b': 2, 'c': 1})
# A partir de un diccionario
c4 = Counter({'rojo': 4, 'azul': 2})
print(c4) # Counter({'rojo': 4, 'azul': 2})
# Creando un Counter vacío
c5 = Counter()
Acceso y actualización
Al ser una subclase de diccionario, Counter
hereda todas sus operaciones básicas:
# Acceder a conteos
frutas = Counter(['manzana', 'naranja', 'manzana', 'plátano', 'manzana'])
print(frutas['manzana']) # 3
print(frutas['pera']) # 0 (no lanza KeyError para elementos inexistentes)
# Actualizar conteos
frutas['naranja'] += 1
print(frutas) # Counter({'manzana': 3, 'naranja': 2, 'plátano': 1})
# Eliminar un elemento
del frutas['plátano']
print(frutas) # Counter({'manzana': 3, 'naranja': 2})
Una característica distintiva de Counter
es que devuelve 0
para elementos inexistentes, en lugar de lanzar un KeyError
como haría un diccionario normal.
Métodos específicos de Counter
Counter
proporciona métodos especializados que facilitan el análisis de frecuencias:
1. most_common()
- Elementos más comunes
texto = "Python es un lenguaje de programación versátil y poderoso"
contador = Counter(texto.lower().split())
# Obtener los 3 elementos más comunes
print(contador.most_common(3)) # [('de', 2), ('python', 1), ('es', 1)]
# Sin argumento devuelve todos los elementos ordenados por frecuencia
print(contador.most_common())
# [('de', 2), ('python', 1), ('es', 1), ('un', 1), ('lenguaje', 1), ('programación', 1), ('versátil', 1), ('y', 1), ('poderoso', 1)]
2. elements()
- Iterar sobre todos los elementos
c = Counter(a=3, b=2, c=1)
print(list(c.elements())) # ['a', 'a', 'a', 'b', 'b', 'c']
# Los elementos con conteo cero o negativo no aparecen
c['d'] = 0
c['e'] = -1
print(list(c.elements())) # ['a', 'a', 'a', 'b', 'b', 'c']
3. update()
- Actualizar conteos
c = Counter(['a', 'b'])
c.update(['a', 'c', 'c'])
print(c) # Counter({'a': 2, 'c': 2, 'b': 1})
# También acepta otro Counter
c.update(Counter({'b': 3, 'd': 1}))
print(c) # Counter({'b': 4, 'a': 2, 'c': 2, 'd': 1})
4. subtract()
- Restar conteos
c = Counter(a=4, b=2, c=0, d=-2)
c.subtract({'a': 1, 'b': 2, 'c': 3})
print(c) # Counter({'a': 3, 'b': 0, 'c': -3, 'd': -2})
Operaciones aritméticas con Counter
Una de las características más potentes de Counter
es la posibilidad de realizar operaciones aritméticas entre ellos:
c1 = Counter(a=3, b=1, c=1)
c2 = Counter(a=1, b=2, d=3)
# Suma: combina conteos
print(c1 + c2) # Counter({'a': 4, 'd': 3, 'b': 3, 'c': 1})
# Resta: mantiene solo conteos positivos
print(c1 - c2) # Counter({'a': 2, 'c': 1})
# Intersección: mínimo de conteos
print(c1 & c2) # Counter({'a': 1, 'b': 1})
# Unión: máximo de conteos
print(c1 | c2) # Counter({'a': 3, 'd': 3, 'b': 2, 'c': 1})
Estas operaciones son muy útiles para comparar distribuciones de frecuencias y encontrar similitudes o diferencias entre conjuntos de datos.
Casos de uso prácticos
1. Análisis de texto
Counter
es ideal para analizar la frecuencia de palabras o caracteres en textos:
import re
from collections import Counter
def analizar_texto(texto):
# Convertir a minúsculas y eliminar signos de puntuación
texto = texto.lower()
palabras = re.findall(r'\b\w+\b', texto)
# Contar palabras
contador = Counter(palabras)
# Mostrar estadísticas
total_palabras = sum(contador.values())
palabras_unicas = len(contador)
print(f"Total de palabras: {total_palabras}")
print(f"Palabras únicas: {palabras_unicas}")
print(f"Palabras más comunes:")
for palabra, freq in contador.most_common(5):
print(f" {palabra}: {freq}")
return contador
texto_ejemplo = """
Python es un lenguaje de programación interpretado cuya filosofía hace
hincapié en la legibilidad de su código. Es un lenguaje multiparadigma,
ya que soporta programación orientada a objetos, programación imperativa
y programación funcional.
"""
analizar_texto(texto_ejemplo)
2. Encontrar elementos duplicados
Counter
facilita la identificación de elementos duplicados en una colección:
def encontrar_duplicados(elementos):
conteo = Counter(elementos)
return {elemento: frecuencia for elemento, frecuencia in conteo.items() if frecuencia > 1}
numeros = [1, 2, 3, 1, 4, 2, 5, 6, 5, 7, 8]
print(encontrar_duplicados(numeros)) # {1: 2, 2: 2, 5: 2}
nombres = ["Ana", "Carlos", "Elena", "Ana", "David", "Carlos"]
print(encontrar_duplicados(nombres)) # {'Ana': 2, 'Carlos': 2}
3. Análisis de datos científicos
En análisis de datos, Counter
puede usarse para calcular distribuciones de frecuencia:
import numpy as np
from collections import Counter
import matplotlib.pyplot as plt
# Generar datos aleatorios
np.random.seed(42)
datos = np.random.normal(loc=5, scale=2, size=1000).round().astype(int)
# Calcular distribución de frecuencia
frecuencias = Counter(datos)
# Visualizar histograma
plt.figure(figsize=(10, 6))
plt.bar(frecuencias.keys(), frecuencias.values())
plt.title('Distribución de frecuencias')
plt.xlabel('Valor')
plt.ylabel('Frecuencia')
plt.grid(True, alpha=0.3)
# plt.show() # Descomentar para mostrar el gráfico
4. Comparación de colecciones
Counter
permite comparar fácilmente el contenido de diferentes colecciones:
def similitud_jaccard(coleccion1, coleccion2):
"""Calcula la similitud de Jaccard entre dos colecciones."""
c1 = Counter(coleccion1)
c2 = Counter(coleccion2)
# Intersección y unión
interseccion = sum((c1 & c2).values())
union = sum((c1 | c2).values())
return interseccion / union if union > 0 else 0
# Comparar conjuntos de etiquetas
tags1 = ['python', 'programación', 'desarrollo', 'web', 'backend']
tags2 = ['python', 'desarrollo', 'frontend', 'javascript', 'web']
print(f"Similitud: {similitud_jaccard(tags1, tags2):.2f}") # Similitud: 0.43
Optimización y rendimiento
Counter
está optimizado para operaciones de conteo y es significativamente más eficiente que implementar soluciones manuales:
import timeit
setup = """
texto = "Python es un lenguaje de programación interpretado cuya filosofía hace hincapié en la legibilidad de su código" * 100
palabras = texto.split()
from collections import Counter
"""
codigo_manual = """
conteo = {}
for palabra in palabras:
if palabra in conteo:
conteo[palabra] += 1
else:
conteo[palabra] = 1
"""
codigo_counter = """
conteo = Counter(palabras)
"""
tiempo_manual = timeit.timeit(codigo_manual, setup=setup, number=1000)
tiempo_counter = timeit.timeit(codigo_counter, setup=setup, number=1000)
print(f"Tiempo con implementación manual: {tiempo_manual:.6f} segundos")
print(f"Tiempo con Counter: {tiempo_counter:.6f} segundos")
print(f"Mejora: {(tiempo_manual - tiempo_counter) / tiempo_manual * 100:.2f}%")
Integración con otras herramientas
Counter
se integra perfectamente con otras bibliotecas del ecosistema Python:
import pandas as pd
from collections import Counter
# Crear un DataFrame a partir de un Counter
ventas_productos = Counter({
'Laptop': 120,
'Smartphone': 250,
'Tablet': 85,
'Auriculares': 175,
'Monitor': 65
})
df = pd.DataFrame(ventas_productos.items(), columns=['Producto', 'Ventas'])
df = df.sort_values('Ventas', ascending=False)
print(df)
Limitaciones y consideraciones
Aunque Counter
es muy versátil, tiene algunas limitaciones:
- No mantiene el orden de inserción (antes de Python 3.7)
- No es adecuado para contar elementos no hashables (como listas o diccionarios)
- Para conteos muy grandes o análisis más complejos, bibliotecas como NumPy o pandas pueden ser más apropiadas
Para casos donde necesitamos contar elementos no hashables, podemos usar técnicas alternativas:
# Contar listas como elementos
listas = [[1, 2], [3, 4], [1, 2], [5, 6]]
contador = {}
for lista in listas:
# Convertir la lista a tupla para hacerla hashable
clave = tuple(lista)
contador[clave] = contador.get(clave, 0) + 1
print(contador) # {(1, 2): 2, (3, 4): 1, (5, 6): 1}
Counter
es una herramienta fundamental para cualquier análisis que involucre frecuencias o distribuciones. Su API intuitiva y su rendimiento optimizado lo convierten en una opción superior a las implementaciones manuales de conteo, simplificando significativamente tareas comunes de procesamiento de datos.
deque
Las estructuras de datos tradicionales como listas en Python tienen limitaciones cuando necesitamos realizar operaciones frecuentes en ambos extremos de una colección. Aquí es donde deque
(double-ended queue o cola de doble extremo) del módulo collections
se convierte en una herramienta fundamental.
deque
es una implementación optimizada de una cola de doble extremo que permite añadir y eliminar elementos tanto por el principio como por el final con una eficiencia O(1), a diferencia de las listas estándar que tienen un rendimiento O(n) para operaciones en el extremo izquierdo.
Creación y operaciones básicas
Para utilizar deque
, primero debemos importarlo del módulo collections:
from collections import deque
# Crear una deque vacía
d1 = deque()
# Crear una deque con elementos iniciales
d2 = deque([1, 2, 3, 4, 5])
# Crear una deque con tamaño máximo (opcional)
d3 = deque(["a", "b", "c"], maxlen=5)
Las operaciones básicas incluyen añadir y eliminar elementos por ambos extremos:
d = deque([1, 2, 3])
# Añadir elementos
d.append(4) # Añade al final: [1, 2, 3, 4]
d.appendleft(0) # Añade al inicio: [0, 1, 2, 3, 4]
# Eliminar elementos
ultimo = d.pop() # Elimina y devuelve el último: 4
primero = d.popleft() # Elimina y devuelve el primero: 0
print(d) # deque([1, 2, 3])
Comparación con listas
Para entender las ventajas de deque
, comparemos su rendimiento con las listas estándar:
import time
# Comparación de rendimiento para insertar al inicio
n = 100000
# Con lista
inicio = time.time()
lista = []
for i in range(n):
lista.insert(0, i) # Inserción al inicio (costosa)
tiempo_lista = time.time() - inicio
# Con deque
inicio = time.time()
d = deque()
for i in range(n):
d.appendleft(i) # Inserción al inicio (eficiente)
tiempo_deque = time.time() - inicio
print(f"Tiempo con lista: {tiempo_lista:.4f} segundos")
print(f"Tiempo con deque: {tiempo_deque:.4f} segundos")
print(f"deque es {tiempo_lista/tiempo_deque:.1f}x más rápido")
La diferencia de rendimiento es notable, especialmente para colecciones grandes.
Características especiales de deque
Tamaño máximo (maxlen)
Una característica única de deque
es la posibilidad de establecer un tamaño máximo:
# Crear una deque con tamaño máximo
historial = deque(maxlen=3)
historial.append("página1") # deque(['página1'])
historial.append("página2") # deque(['página1', 'página2'])
historial.append("página3") # deque(['página1', 'página2', 'página3'])
# Al añadir un cuarto elemento, se elimina automáticamente el primero
historial.append("página4") # deque(['página2', 'página3', 'página4'])
Esta funcionalidad es perfecta para implementar buffers circulares o mantener un historial limitado de elementos.
Rotación de elementos
deque
permite rotar sus elementos en cualquier dirección:
d = deque([1, 2, 3, 4, 5])
# Rotar a la derecha (los elementos se mueven hacia la derecha)
d.rotate(2)
print(d) # deque([4, 5, 1, 2, 3])
# Rotar a la izquierda (con número negativo)
d.rotate(-1)
print(d) # deque([5, 1, 2, 3, 4])
Esta operación es muy útil para algoritmos que requieren desplazamiento cíclico de elementos.
Casos de uso prácticos
1. Historial con capacidad limitada
Ideal para mantener un registro de las últimas N acciones:
def registrar_actividad(historial, accion):
"""Registra una acción en el historial con capacidad limitada."""
timestamp = time.strftime("%H:%M:%S")
historial.append(f"[{timestamp}] {accion}")
return historial
# Historial de las últimas 5 acciones
historial_acciones = deque(maxlen=5)
# Simulación de actividad
registrar_actividad(historial_acciones, "Usuario inició sesión")
registrar_actividad(historial_acciones, "Usuario actualizó perfil")
registrar_actividad(historial_acciones, "Usuario subió archivo")
registrar_actividad(historial_acciones, "Usuario envió mensaje")
registrar_actividad(historial_acciones, "Usuario descargó documento")
registrar_actividad(historial_acciones, "Usuario cerró sesión")
# Mostrar historial (solo contiene las últimas 5 acciones)
for accion in historial_acciones:
print(accion)
2. Algoritmo de ventana deslizante
deque
es perfecto para implementar algoritmos de ventana deslizante:
def promedio_movil(datos, tamano_ventana):
"""Calcula el promedio móvil de una serie de datos."""
resultados = []
ventana = deque(maxlen=tamano_ventana)
for x in datos:
ventana.append(x)
if len(ventana) == tamano_ventana:
resultados.append(sum(ventana) / tamano_ventana)
return resultados
# Datos de temperatura por hora
temperaturas = [22, 23, 25, 26, 24, 23, 22, 21, 20, 19]
# Calcular promedio móvil con ventana de 3 horas
promedios = promedio_movil(temperaturas, 3)
print(promedios) # [23.33, 24.67, 25.0, 24.33, 23.0, 22.0, 21.0, 20.0]
3. Implementación de colas y pilas
deque
puede funcionar eficientemente como cola o pila:
# Como cola (FIFO: First In, First Out)
cola = deque()
cola.append("Tarea 1") # Añadir al final
cola.append("Tarea 2")
cola.append("Tarea 3")
# Procesar tareas en orden de llegada
while cola:
tarea = cola.popleft() # Sacar del inicio
print(f"Procesando: {tarea}")
# Como pila (LIFO: Last In, First Out)
pila = deque()
pila.append("Página A")
pila.append("Página B")
pila.append("Página C")
# Navegar hacia atrás
while pila:
pagina = pila.pop() # Sacar del final
print(f"Volviendo a: {pagina}")
4. Búsqueda de patrones en secuencias
deque
facilita la búsqueda de patrones en secuencias de datos:
def encontrar_patron(secuencia, patron):
"""Encuentra todas las ocurrencias de un patrón en una secuencia."""
n, m = len(secuencia), len(patron)
if m > n:
return []
ventana = deque(secuencia[:m])
coincidencias = []
# Comprobar primera ventana
if list(ventana) == patron:
coincidencias.append(0)
# Deslizar ventana
for i in range(m, n):
ventana.popleft()
ventana.append(secuencia[i])
if list(ventana) == patron:
coincidencias.append(i - m + 1)
return coincidencias
# Buscar patrón [1, 2, 3] en una secuencia
secuencia = [4, 1, 2, 3, 5, 1, 2, 3, 7]
patron = [1, 2, 3]
posiciones = encontrar_patron(secuencia, patron)
print(f"Patrón encontrado en posiciones: {posiciones}") # [1, 5]
Métodos adicionales
deque
ofrece otros métodos útiles para manipular la colección:
d = deque([1, 2, 3, 4])
# Extender por ambos extremos
d.extend([5, 6]) # Añade varios elementos al final
print(d) # deque([1, 2, 3, 4, 5, 6])
d.extendleft([0, -1]) # Añade varios elementos al inicio (en orden inverso)
print(d) # deque([-1, 0, 1, 2, 3, 4, 5, 6])
# Eliminar elementos específicos
d.remove(3) # Elimina la primera ocurrencia de 3
print(d) # deque([-1, 0, 1, 2, 4, 5, 6])
# Contar ocurrencias
d.append(1)
print(d.count(1)) # 2 (hay dos elementos con valor 1)
# Limpiar la deque
d.clear()
print(d) # deque([])
Implementación de un buffer circular
Un buffer circular es una estructura de datos de tamaño fijo que sobrescribe los elementos más antiguos cuando está lleno. deque
con maxlen
es ideal para esta implementación:
class BufferCircular:
def __init__(self, capacidad):
self.buffer = deque(maxlen=capacidad)
def agregar(self, elemento):
self.buffer.append(elemento)
def obtener_ultimos(self, n=None):
if n is None or n >= len(self.buffer):
return list(self.buffer)
return list(self.buffer)[-n:]
def __len__(self):
return len(self.buffer)
def __str__(self):
return str(list(self.buffer))
# Ejemplo: buffer de mensajes de log
buffer_log = BufferCircular(5)
mensajes = [
"Inicio de aplicación",
"Conexión a base de datos establecida",
"Usuario admin autenticado",
"Consulta ejecutada: SELECT * FROM usuarios",
"Datos exportados a CSV",
"Sesión cerrada",
"Aplicación finalizada"
]
for mensaje in mensajes:
buffer_log.agregar(mensaje)
print(f"Buffer actual ({len(buffer_log)}/{buffer_log.buffer.maxlen}): {buffer_log}")
# Obtener los últimos 3 mensajes
print("\nÚltimos 3 mensajes:")
for mensaje in buffer_log.obtener_ultimos(3):
print(f"- {mensaje}")
Consideraciones de rendimiento
deque
está implementado como una lista doblemente enlazada de bloques, lo que permite operaciones eficientes en ambos extremos:
import sys
# Comparar tamaño en memoria
lista = list(range(1000))
d = deque(range(1000))
print(f"Tamaño de lista: {sys.getsizeof(lista)} bytes")
print(f"Tamaño de deque: {sys.getsizeof(d)} bytes")
Aunque deque
puede ocupar más memoria que una lista, su eficiencia en operaciones de inserción y eliminación en ambos extremos compensa este costo para muchos casos de uso.
Integración con otras estructuras de collections
deque
se complementa bien con otras estructuras del módulo collections
:
from collections import deque, Counter
# Análisis de secuencias deslizantes
texto = "abracadabra"
n = 3 # Tamaño de la ventana
# Usar deque para mantener una ventana deslizante
ventana = deque(maxlen=n)
frecuencias = Counter()
for c in texto:
ventana.append(c)
if len(ventana) == n:
# Convertir la ventana actual a una cadena
subcadena = ''.join(ventana)
frecuencias[subcadena] += 1
# Mostrar las subcadenas más comunes
print("Subcadenas más frecuentes de longitud 3:")
for subcadena, frecuencia in frecuencias.most_common():
print(f"'{subcadena}': {frecuencia}")
Limitaciones
Aunque deque
es muy versátil, tiene algunas limitaciones:
- No permite acceso aleatorio eficiente como las listas (operaciones como
d[i]
son O(n) en el peor caso) - No soporta rebanado (slicing) como
d[1:3]
- Consume más memoria que una lista para el mismo número de elementos
Para casos donde necesitamos tanto acceso aleatorio eficiente como operaciones rápidas en los extremos, podríamos considerar otras estructuras de datos o combinar diferentes estructuras.
deque
es una estructura de datos potente y versátil que resuelve eficientemente problemas que requieren manipulación de elementos por ambos extremos. Su implementación optimizada la convierte en la opción ideal para colas, pilas, buffers circulares y algoritmos de ventana deslizante, superando significativamente a las listas estándar en estos escenarios.
Ejercicios de esta lección Módulo collections
Evalúa tus conocimientos de esta lección Módulo collections con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Módulo math
Reto herencia
Excepciones
Introducción a Python
Reto variables
Funciones Python
Reto funciones
Módulo datetime
Reto acumulación
Reto estructuras condicionales
Polimorfismo
Módulo os
Reto métodos dunder
Diccionarios
Reto clases y objetos
Reto operadores
Operadores
Estructuras de control
Funciones lambda
Reto diccionarios
Reto función lambda
Encapsulación
Reto coleciones
Reto funciones auxiliares
Crear módulos y paquetes
Módulo datetime
Excepciones
Operadores
Diccionarios
Reto map, filter
Reto tuplas
Proyecto gestor de tareas CRUD
Tuplas
Variables
Tipos de datos
Conjuntos
Reto mixins
Módulo csv
Módulo json
Herencia
Análisis de datos de ventas con Pandas
Reto fechas y tiempo
Reto estructuras de iteración
Funciones
Reto comprehensions
Variables
Reto serialización
Módulo csv
Reto polimorfismo
Polimorfismo
Clases y objetos
Reto encapsulación
Estructuras de control
Importar módulos y paquetes
Módulo math
Funciones lambda
Reto excepciones
Listas
Reto archivos
Encapsulación
Reto conjuntos
Clases y objetos
Instalación de Python y creación de proyecto
Reto listas
Tipos de datos
Crear módulos y paquetes
Tuplas
Herencia
Reto acceso a sistema
Proyecto sintaxis calculadora
Importar módulos y paquetes
Clases y objetos
Módulo os
Listas
Conjuntos
Reto tipos de datos
Reto matemáticas
Módulo json
Todas las lecciones de Python
Accede a todas las lecciones de Python y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Python
Introducción
Instalación Y Creación De Proyecto
Introducción
Tema 2: Tipos De Datos, Variables Y Operadores
Introducción
Instalación De Python
Introducción
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Estructuras Control Iterativo
Sintaxis
Estructuras Control Condicional
Sintaxis
Testing Con Pytest
Sintaxis
Listas
Estructuras De Datos
Tuplas
Estructuras De Datos
Diccionarios
Estructuras De Datos
Conjuntos
Estructuras De Datos
Comprehensions
Estructuras De Datos
Clases Y Objetos
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Mixins Y Herencia Múltiple
Programación Orientada A Objetos
Métodos Especiales (Dunder Methods)
Programación Orientada A Objetos
Composición De Clases
Programación Orientada A Objetos
Funciones Lambda
Programación Funcional
Aplicación Parcial
Programación Funcional
Entrada Y Salida, Manejo De Archivos
Programación Funcional
Decoradores
Programación Funcional
Generadores
Programación Funcional
Paradigma Funcional
Programación Funcional
Composición De Funciones
Programación Funcional
Funciones Orden Superior Map Y Filter
Programación Funcional
Funciones Auxiliares
Programación Funcional
Reducción Y Acumulación
Programación Funcional
Archivos Comprimidos
Entrada Y Salida Io
Entrada Y Salida Avanzada
Entrada Y Salida Io
Archivos Temporales
Entrada Y Salida Io
Contexto With
Entrada Y Salida Io
Módulo Csv
Biblioteca Estándar
Módulo Json
Biblioteca Estándar
Módulo Datetime
Biblioteca Estándar
Módulo Math
Biblioteca Estándar
Módulo Os
Biblioteca Estándar
Módulo Re
Biblioteca Estándar
Módulo Random
Biblioteca Estándar
Módulo Time
Biblioteca Estándar
Módulo Collections
Biblioteca Estándar
Módulo Sys
Biblioteca Estándar
Módulo Statistics
Biblioteca Estándar
Módulo Pickle
Biblioteca Estándar
Módulo Pathlib
Biblioteca Estándar
Importar Módulos Y Paquetes
Paquetes Y Módulos
Crear Módulos Y Paquetes
Paquetes Y Módulos
Entornos Virtuales (Virtualenv, Venv)
Entorno Y Dependencias
Gestión De Dependencias (Pip, Requirements.txt)
Entorno Y Dependencias
Python-dotenv Y Variables De Entorno
Entorno Y Dependencias
Acceso A Datos Con Mysql, Pymongo Y Pandas
Acceso A Bases De Datos
Acceso A Mongodb Con Pymongo
Acceso A Bases De Datos
Acceso A Mysql Con Mysql Connector
Acceso A Bases De Datos
Novedades Python 3.13
Características Modernas
Operador Walrus
Características Modernas
Pattern Matching
Características Modernas
Instalación Beautiful Soup
Web Scraping
Sintaxis General De Beautiful Soup
Web Scraping
Tipos De Selectores
Web Scraping
Web Scraping De Html
Web Scraping
Web Scraping Para Ciencia De Datos
Web Scraping
Autenticación Y Acceso A Recursos Protegidos
Web Scraping
Combinación De Selenium Con Beautiful Soup
Web Scraping
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender la utilidad y creación de namedtuple para estructuras de datos inmutables con campos nombrados.
- Aprender a utilizar defaultdict para diccionarios con valores por defecto y evitar comprobaciones manuales.
- Explorar las funcionalidades de Counter para conteo eficiente de elementos y análisis de frecuencias.
- Conocer las ventajas de deque para manipulación eficiente de colecciones en ambos extremos.
- Aplicar estas estructuras en casos prácticos y entender sus limitaciones y rendimiento.