Python

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ícate

namedtuple

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.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende Python online

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

Python
Puzzle

Reto herencia

Python
Código

Excepciones

Python
Test

Introducción a Python

Python
Test

Reto variables

Python
Código

Funciones Python

Python
Puzzle

Reto funciones

Python
Código

Módulo datetime

Python
Test

Reto acumulación

Python
Código

Reto estructuras condicionales

Python
Código

Polimorfismo

Python
Test

Módulo os

Python
Test

Reto métodos dunder

Python
Código

Diccionarios

Python
Puzzle

Reto clases y objetos

Python
Código

Reto operadores

Python
Código

Operadores

Python
Test

Estructuras de control

Python
Puzzle

Funciones lambda

Python
Test

Reto diccionarios

Python
Código

Reto función lambda

Python
Código

Encapsulación

Python
Puzzle

Reto coleciones

Python
Proyecto

Reto funciones auxiliares

Python
Código

Crear módulos y paquetes

Python
Puzzle

Módulo datetime

Python
Puzzle

Excepciones

Python
Puzzle

Operadores

Python
Puzzle

Diccionarios

Python
Test

Reto map, filter

Python
Código

Reto tuplas

Python
Código

Proyecto gestor de tareas CRUD

Python
Proyecto

Tuplas

Python
Puzzle

Variables

Python
Puzzle

Tipos de datos

Python
Puzzle

Conjuntos

Python
Test

Reto mixins

Python
Código

Módulo csv

Python
Test

Módulo json

Python
Test

Herencia

Python
Test

Análisis de datos de ventas con Pandas

Python
Proyecto

Reto fechas y tiempo

Python
Proyecto

Reto estructuras de iteración

Python
Código

Funciones

Python
Test

Reto comprehensions

Python
Código

Variables

Python
Test

Reto serialización

Python
Proyecto

Módulo csv

Python
Puzzle

Reto polimorfismo

Python
Código

Polimorfismo

Python
Puzzle

Clases y objetos

Python
Código

Reto encapsulación

Python
Código

Estructuras de control

Python
Test

Importar módulos y paquetes

Python
Test

Módulo math

Python
Test

Funciones lambda

Python
Puzzle

Reto excepciones

Python
Código

Listas

Python
Puzzle

Reto archivos

Python
Proyecto

Encapsulación

Python
Test

Reto conjuntos

Python
Código

Clases y objetos

Python
Test

Instalación de Python y creación de proyecto

Python
Test

Reto listas

Python
Código

Tipos de datos

Python
Test

Crear módulos y paquetes

Python
Test

Tuplas

Python
Test

Herencia

Python
Puzzle

Reto acceso a sistema

Python
Proyecto

Proyecto sintaxis calculadora

Python
Proyecto

Importar módulos y paquetes

Python
Puzzle

Clases y objetos

Python
Puzzle

Módulo os

Python
Puzzle

Listas

Python
Test

Conjuntos

Python
Puzzle

Reto tipos de datos

Python
Código

Reto matemáticas

Python
Proyecto

Módulo json

Python
Puzzle

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

Python

Introducción

Instalación Y Creación De Proyecto

Python

Introducción

Tema 2: Tipos De Datos, Variables Y Operadores

Python

Introducción

Instalación De Python

Python

Introducción

Tipos De Datos

Python

Sintaxis

Variables

Python

Sintaxis

Operadores

Python

Sintaxis

Estructuras De Control

Python

Sintaxis

Funciones

Python

Sintaxis

Estructuras Control Iterativo

Python

Sintaxis

Estructuras Control Condicional

Python

Sintaxis

Testing Con Pytest

Python

Sintaxis

Listas

Python

Estructuras De Datos

Tuplas

Python

Estructuras De Datos

Diccionarios

Python

Estructuras De Datos

Conjuntos

Python

Estructuras De Datos

Comprehensions

Python

Estructuras De Datos

Clases Y Objetos

Python

Programación Orientada A Objetos

Excepciones

Python

Programación Orientada A Objetos

Encapsulación

Python

Programación Orientada A Objetos

Herencia

Python

Programación Orientada A Objetos

Polimorfismo

Python

Programación Orientada A Objetos

Mixins Y Herencia Múltiple

Python

Programación Orientada A Objetos

Métodos Especiales (Dunder Methods)

Python

Programación Orientada A Objetos

Composición De Clases

Python

Programación Orientada A Objetos

Funciones Lambda

Python

Programación Funcional

Aplicación Parcial

Python

Programación Funcional

Entrada Y Salida, Manejo De Archivos

Python

Programación Funcional

Decoradores

Python

Programación Funcional

Generadores

Python

Programación Funcional

Paradigma Funcional

Python

Programación Funcional

Composición De Funciones

Python

Programación Funcional

Funciones Orden Superior Map Y Filter

Python

Programación Funcional

Funciones Auxiliares

Python

Programación Funcional

Reducción Y Acumulación

Python

Programación Funcional

Archivos Comprimidos

Python

Entrada Y Salida Io

Entrada Y Salida Avanzada

Python

Entrada Y Salida Io

Archivos Temporales

Python

Entrada Y Salida Io

Contexto With

Python

Entrada Y Salida Io

Módulo Csv

Python

Biblioteca Estándar

Módulo Json

Python

Biblioteca Estándar

Módulo Datetime

Python

Biblioteca Estándar

Módulo Math

Python

Biblioteca Estándar

Módulo Os

Python

Biblioteca Estándar

Módulo Re

Python

Biblioteca Estándar

Módulo Random

Python

Biblioteca Estándar

Módulo Time

Python

Biblioteca Estándar

Módulo Collections

Python

Biblioteca Estándar

Módulo Sys

Python

Biblioteca Estándar

Módulo Statistics

Python

Biblioteca Estándar

Módulo Pickle

Python

Biblioteca Estándar

Módulo Pathlib

Python

Biblioteca Estándar

Importar Módulos Y Paquetes

Python

Paquetes Y Módulos

Crear Módulos Y Paquetes

Python

Paquetes Y Módulos

Entornos Virtuales (Virtualenv, Venv)

Python

Entorno Y Dependencias

Gestión De Dependencias (Pip, Requirements.txt)

Python

Entorno Y Dependencias

Python-dotenv Y Variables De Entorno

Python

Entorno Y Dependencias

Acceso A Datos Con Mysql, Pymongo Y Pandas

Python

Acceso A Bases De Datos

Acceso A Mongodb Con Pymongo

Python

Acceso A Bases De Datos

Acceso A Mysql Con Mysql Connector

Python

Acceso A Bases De Datos

Novedades Python 3.13

Python

Características Modernas

Operador Walrus

Python

Características Modernas

Pattern Matching

Python

Características Modernas

Instalación Beautiful Soup

Python

Web Scraping

Sintaxis General De Beautiful Soup

Python

Web Scraping

Tipos De Selectores

Python

Web Scraping

Web Scraping De Html

Python

Web Scraping

Web Scraping Para Ciencia De Datos

Python

Web Scraping

Autenticación Y Acceso A Recursos Protegidos

Python

Web Scraping

Combinación De Selenium Con Beautiful Soup

Python

Web Scraping

Accede GRATIS a Python y certifícate

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.