Listas, tuplas, diccionarios, conjuntos y cadenas: el arsenal básico está completo, y el catálogo de Papyrus vive por fin en una sola estructura. Pero la biblioteca estándar (03-05) guarda un módulo pensado exactamente para pulir los patrones que hemos ido improvisando: collections. Aquí saldamos la promesa de 04-02 con namedtuple a fondo, contamos los libros más vendidos con Counter, agrupamos pedidos por cliente con defaultdict y montamos una cola de reservas eficiente con deque — y cerramos el módulo con la tabla de criterio definitiva: qué estructura elegir en cada situación.
Contenido
namedtuple: registros con nombre, a fondoCounter: contar sin buclesdefaultdict: diccionarios con valor por defectodeque: colas eficientes por los dos extremosOrderedDict: nota histórica- Criterios de elección: la tabla-resumen del módulo
namedtuple: registros con nombre, a fondo
En 04-02 la probamos como aperitivo; ahora, el plato completo. Una namedtuple es una fábrica de clases de tuplas: defines el nombre del registro y sus campos, y obtienes un tipo nuevo cuyas instancias son tuplas con campos accesibles por nombre.
from collections import namedtuple
Libro = namedtuple("Libro", ["titulo", "precio", "stock"])
fausto = Libro("Fausto", 21.00, 0)
odisea = Libro(titulo="La Odisea", precio=12.50, stock=4) # también con keywords
print(fausto.precio) # 21.0 → por nombre: se acabaron los índices mudos
print(fausto[1]) # 21.0 → sigue siendo una tupla: índice, slicing, len()...
titulo, precio, stock = fausto # y desempaquetado
print(fausto) # Libro(titulo='Fausto', precio=21.0, stock=0) → repr legible ¡gratis!Tres utilidades extra que conviene conocer:
print(fausto._asdict()) # {'titulo': 'Fausto', 'precio': 21.0, 'stock': 0} → a dict
rebajado = fausto._replace(precio=18.90) # "modificar" un inmutable: crea OTRA namedtuple
print(rebajado.precio, fausto.precio) # 18.9 21.0 → el original, intacto
Libro2 = namedtuple("Libro2", "titulo precio stock") # los campos también valen como cadena¿Y el catálogo? La namedtuple compite con el dict anidado de 04-03, y la elección es una decisión de diseño real:
# Opción A (04-03): dict de dicts — flexible, campos como cadenas
catalogo = {"Fausto": {"precio": 21.00, "stock": 0}}
# Opción B: dict de namedtuples — campos con nombre real y protegidos contra typos
catalogo_nt = {"Fausto": Libro("Fausto", 21.00, 0)}
print(catalogo_nt["Fausto"].precio) # 21.0
# catalogo_nt["Fausto"].stock = 3 # AttributeError: es inmutableCon la opción B, un error tipográfico (ficha["precoi"]) que en el dict crearía silenciosamente una clave nueva, aquí falla al instante (AttributeError). A cambio, actualizar el stock exige _replace() y reasignar — incómodo para datos que cambian a cada venta. Para Papyrus mantenemos el dict anidado como estructura canónica mutable y usamos namedtuple para registros de solo lectura, como las líneas de un ticket ya cobrado.
Counter: contar sin bucles
Contar apariciones es tan común que collections lo trae resuelto. Counter es un diccionario especializado: claves = elementos, valores = cuántas veces aparecen.
from collections import Counter
# Ventas del mes en Papyrus, tal como se registraron
ventas = ["Hamlet", "El Quijote", "Hamlet", "La Odisea", "Hamlet", "El Quijote"]
contador = Counter(ventas)
print(contador) # Counter({'Hamlet': 3, 'El Quijote': 2, 'La Odisea': 1})
print(contador["Hamlet"]) # 3
print(contador["Fausto"]) # 0 → clave inexistente devuelve 0, NO KeyError
print(contador.most_common(2)) # [('Hamlet', 3), ('El Quijote', 2)] → top ventas, ordenado
contador.update(["Fausto", "Hamlet"]) # llegan más ventas
print(contador["Hamlet"]) # 4
print(sum(contador.values())) # 8 → total de ejemplares vendidos
letras = Counter("papyrus") # también cuenta caracteres de una cadenaCompara con la alternativa manual — el bucle con if titulo in cuentas: ... else: ... que habrías escrito en el módulo 2 — y aprecia la diferencia: most_common() te da el ranking de superventas de Ana en una llamada, listo para el escaparate. Como es un dict con esteroides, todo lo de 04-03 funciona: items(), comprensiones, in...
defaultdict: diccionarios con valor por defecto
Patrón clásico: agrupar cosas. Ana quiere los pedidos organizados por cliente. Con un dict normal hay que comprobar si la clave existe antes de hacer append:
pedidos = [("Luis", "El Quijote"), ("Marta", "Hamlet"),
("Luis", "Fausto"), ("Marta", "La Odisea"), ("Luis", "Hamlet")]
# Con dict normal: la comprobación estorba
por_cliente = {}
for cliente, titulo in pedidos:
if cliente not in por_cliente:
por_cliente[cliente] = [] # la ceremonia de inicializar
por_cliente[cliente].append(titulo)defaultdict elimina la ceremonia: se construye con una fábrica (una función sin argumentos, como list, int o set) que se invoca automáticamente la primera vez que se accede a una clave inexistente:
from collections import defaultdict
por_cliente = defaultdict(list) # clave nueva → se crea [] automáticamente
for cliente, titulo in pedidos:
por_cliente[cliente].append(titulo) # sin if: la primera vez crea la lista sola
print(dict(por_cliente))
# {'Luis': ['El Quijote', 'Fausto', 'Hamlet'], 'Marta': ['Hamlet', 'La Odisea']}
gasto = defaultdict(float) # float() → 0.0: acumuladores sin inicializar
gasto["Luis"] += 15.71
gasto["Luis"] += 20.75
print(gasto["Luis"]) # 36.46| Fábrica | Valor inicial | Uso típico |
|---|---|---|
list |
[] |
Agrupar (append por clave) |
int |
0 |
Contar (aunque Counter suele ser mejor) |
float |
0.0 |
Acumular importes |
set |
set() |
Agrupar sin duplicados |
Matiz importante: leer también crea. por_cliente["Nadie"] inserta "Nadie": [] solo por consultarlo. Si vas a hacer muchas consultas de solo lectura, conviértelo antes a dict normal (dict(por_cliente)) o usa get().
deque: colas eficientes por los dos extremos
En 04-01 montamos la cola de pedidos con una lista y avisamos: pop(0) desplaza todos los elementos restantes. deque (double-ended queue, se pronuncia "dec") está optimizada para añadir y quitar por ambos extremos sin desplazar nada:
from collections import deque
reservas = deque() # la cola de reservas de "Fausto" (agotado, ¿recuerdas?)
reservas.append("Luis") # entra por la derecha, como en una lista
reservas.append("Marta")
reservas.appendleft("Pau") # ¡urgente! entra por la izquierda
print(reservas) # deque(['Pau', 'Luis', 'Marta'])
siguiente = reservas.popleft() # sale por la izquierda: el primero de la cola
print(siguiente) # 'Pau' → llegan ejemplares de Fausto: aviso a Pau
ultimo = reservas.pop() # también puede salir por la derecha
ultimas_ventas = deque(maxlen=3) # con maxlen: al llenarse, expulsa por el otro extremo
for t in ["Hamlet", "La Odisea", "El Quijote", "Fausto"]:
ultimas_ventas.append(t)
print(ultimas_ventas) # deque(['La Odisea', 'El Quijote', 'Fausto'], maxlen=3)El maxlen es una joya para "las N últimas operaciones" (historial del menú de Papyrus, por ejemplo): se mantiene solo. La contrapartida de deque: acceder por índice al medio (reservas[500]) es lento comparado con una lista. Regla: extremos → deque; posiciones interiores → lista.
graph LR
A["appendleft()"] --> D["deque: Pau · Luis · Marta"]
D --> B["pop()"]
C["popleft()"] --- D
D --- E["append()"]
OrderedDict: nota histórica
En código antiguo verás collections.OrderedDict: un dict que garantizaba el orden de inserción cuando los dicts normales no lo hacían. Desde Python 3.7 todos los dict conservan el orden, así que hoy casi nunca lo necesitas. Sobrevive por compatibilidad y por dos detalles menores (su igualdad sí compara el orden, y tiene move_to_end()). Si lo ves en un tutorial, ya sabes por qué está ahí — y que puedes usar un dict normal.
Criterios de elección: la tabla-resumen del módulo
La decisión de estructura es la primera decisión de diseño de cualquier programa. Todo el módulo 4, en una tabla:
| Estructura | Ordenada | Mutable | Duplicados | Acceso | Elígela cuando... |
|---|---|---|---|---|---|
list |
Sí | Sí | Sí | Por índice | Colección homogénea que crece/mengua/se ordena (cola de pedidos, títulos) |
tuple |
Sí | No | Sí | Por índice | Registro fijo y heterogéneo; retorno múltiple; clave compuesta de dict |
dict |
Inserción | Sí | Claves no | Por clave | Asociar identidad → datos: el catálogo {título: ficha} |
set |
No | Sí | No | Pertenencia | Unicidad, in masivo, álgebra de conjuntos (socios por mes) |
frozenset |
No | No | No | Pertenencia | Un set que deba ser clave o constante |
str |
Sí | No | Sí | Por índice | Texto; normalizar con sus métodos; construir con join() |
namedtuple |
Sí | No | Sí | Por nombre | Registro de solo lectura con campos legibles (línea de ticket) |
Counter |
— | Sí | (cuenta) | Por clave | Contar apariciones y rankings (most_common) |
defaultdict |
Inserción | Sí | Claves no | Por clave | Agrupar/acumular sin inicializar claves |
deque |
Sí | Sí | Sí | Extremos | Colas y "últimos N" (reservas de Fausto) |
Y como guía de decisión rápida:
graph TD
A["¿Qué necesito guardar?"] --> B{"¿Pares clave→valor?"}
B -- "Sí" --> C{"¿Claves que pueden faltar al agrupar?"}
C -- "Sí" --> D["defaultdict"]
C -- "¿Es contar?" --> E["Counter"]
C -- "No" --> F["dict"]
B -- "No" --> G{"¿Importan orden y posición?"}
G -- "No: solo unicidad/pertenencia" --> H["set / frozenset"]
G -- "Sí" --> I{"¿Cambiará tras crearse?"}
I -- "Sí" --> J{"¿Entra/sale por los extremos?"}
J -- "Sí" --> K["deque"]
J -- "No" --> L["list"]
I -- "No" --> M{"¿Campos con significado propio?"}
M -- "Sí" --> N["namedtuple"]
M -- "No" --> O["tuple"]
Errores Comunes y Consejos
- Olvidar el import:
NameError: name 'Counter' is not defined. Todo lo de esta lección vive encollections:from collections import Counter, defaultdict, deque, namedtuple. - Mutar una
namedtuple:libro.stock = 3lanzaAttributeError. Es una tupla: usa_replace()y reasigna, o reconoce que ese dato pedía un dict. defaultdictque "engorda" solo: cada lectura de clave nueva la crea. Para consultar sin crear,d.get(clave)o convertir adictal terminar de construir.- Usar
Countercomo validador de existencia:contador["X"]devuelve0en vez deKeyError, así queif contador["X"]:es correcto perocontador["X"]jamás delatará una clave mal escrita. - Indexar el medio de una
dequeen bucle: para eso es mala; si necesitas posiciones interiores, era una lista. - Elegir estructura por costumbre ("todo listas"): revisa la tabla. La estructura correcta elimina código — el gran refactor de 04-03 borró más líneas de las que añadió.
- Consejo:
Counter,defaultdictydequeson dicts/colas normales en su interfaz; todo lo aprendido en 04-03 y 04-01 les aplica. No son estructuras nuevas que aprender, sino atajos sobre las que ya dominas.
Ejercicios
- Ranking de superventas. Con
ventas = ["Hamlet", "El Quijote", "Hamlet", "La Odisea", "Hamlet", "El Quijote", "Fausto"], usaCounterpara imprimir el podio (top 3) con formato1. Hamlet — 3 uds.usandomost_common()yenumerate(start=1)(módulo 2, al rescate). Añade el total de ejemplares vendidos. - Agrupador de reservas. Con la lista de tuplas
reservas = [("Fausto", "Luis"), ("Fausto", "Marta"), ("Ilíada", "Luis"), ("Fausto", "Pau")], construye condefaultdict(list)el dicttitulo → [clientes en orden]. Después convierte la lista de "Fausto" en unadequey simula la llegada de 2 ejemplares: hazpopleft()dos veces e informa a quién se avisa y quién sigue esperando. - Ticket inmutable. Define la
namedtupleLineaTicketcon campostitulo,unidadeseimporte. Crea las líneas de una compra de Luis (2 "Hamlet" a 9.83 cada uno → importe 19.66; 1 "El Quijote" → 15.71), guárdalas en una lista e imprime el ticket con f-strings alineadas (04-05) y el total consum()y una expresión generadora sobre.importe.
Soluciones
# Ejercicio 1
from collections import Counter
ventas = ["Hamlet", "El Quijote", "Hamlet", "La Odisea", "Hamlet", "El Quijote", "Fausto"]
contador = Counter(ventas)
for puesto, (titulo, unidades) in enumerate(contador.most_common(3), start=1):
print(f"{puesto}. {titulo} — {unidades} uds.")
print(f"Total vendidos: {sum(contador.values())}")
# 1. Hamlet — 3 uds. / 2. El Quijote — 2 uds. / 3. La Odisea — 1 uds. / Total: 7Fíjate: most_common(3) devuelve tuplas (titulo, unidades), y el for las desempaqueta dentro del par de enumerate — desempaquetado anidado con paréntesis.
# Ejercicio 2
from collections import defaultdict, deque
reservas = [("Fausto", "Luis"), ("Fausto", "Marta"), ("Ilíada", "Luis"), ("Fausto", "Pau")]
por_titulo = defaultdict(list)
for titulo, cliente in reservas:
por_titulo[titulo].append(cliente)
cola_fausto = deque(por_titulo["Fausto"]) # deque(['Luis', 'Marta', 'Pau'])
for _ in range(2): # llegan 2 ejemplares
print(f"Avisar a {cola_fausto.popleft()}: su Fausto ha llegado")
print(f"Siguen esperando: {list(cola_fausto)}") # ['Pau']# Ejercicio 3
from collections import namedtuple
LineaTicket = namedtuple("LineaTicket", ["titulo", "unidades", "importe"])
lineas = [
LineaTicket("Hamlet", 2, 19.66),
LineaTicket("El Quijote", 1, 15.71),
]
for linea in lineas:
print(f"{linea.unidades} x {linea.titulo:<12}{linea.importe:>8.2f} EUR")
total = sum(linea.importe for linea in lineas)
print(f"{'TOTAL':<16}{total:>8.2f} EUR") # 35.37 EURConsejo: las líneas de un ticket cobrado no deben cambiar jamás — la inmutabilidad de la namedtuple no es una limitación aquí, es exactamente la garantía que el negocio necesita.
Conclusión
El módulo 4 cumple lo que prometió al cerrar el módulo 3: cada libro de Papyrus es por fin una sola pieza de información. Repasaste las listas a fondo con su mutabilidad y sus trampas de alias (04-01); las tuplas te dieron registros inmutables y el retorno múltiple que 03-02 dejó prometido (04-02); los diccionarios ejecutaron el gran refactor — el catálogo es hoy {título: {"precio": ..., "stock": ...}} con acceso directo por clave (04-03); los conjuntos aportaron unicidad, pertenencia ultrarrápida y álgebra para comparar socios y deduplicar pedidos (04-04); las cadenas revelaron su caja de herramientas completa y la normalización definitiva de buscar_libro() (04-05); y collections puso nombre a los patrones que faltaban — namedtuple, Counter, defaultdict, deque — junto con la tabla de criterio para elegir estructura sin dudar. Pero mira el código de Papyrus con ojos nuevos: el catálogo vive en un diccionario y las funciones que saben operarlo — buscar_libro(), mostrar_catalogo(), precio_final() — viven aparte, en papyrus_utils.py, confiando en que nadie les pase una estructura equivocada. Datos por un lado, comportamiento por otro. El módulo 5 presenta la herramienta que los une en una sola pieza con nombre propio: las clases. Cada libro dejará de ser una entrada de diccionario para convertirse en un objeto que sabe calcular su propio precio de socio — la programación orientada a objetos empieza donde las estructuras de datos terminan.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
