La lección anterior terminó con una pregunta: ¿por qué consultar catalogo["Fausto"] es prácticamente instantáneo? La respuesta vive en una estructura hermana del diccionario: el conjunto (set). Un conjunto es una colección sin orden garantizado, sin duplicados y sin valores asociados — solo elementos, como las claves de un diccionario sin sus valores. A cambio de renunciar al orden y a la repetición, ofrece dos superpoderes: comprobar pertenencia a toda velocidad y operar con la lógica de la teoría de conjuntos (unión, intersección, diferencia) que en Papyrus resolverá preguntas como "¿qué socios compraron este mes pero no el pasado?" en una sola línea.
Contenido
- Crear conjuntos (y la trampa del
{}vacío) - Unicidad: los duplicados desaparecen
- Añadir y eliminar elementos
- Pertenencia ultrarrápida: por qué
invuela - Operaciones de conjuntos: unión, intersección, diferencias
- Subconjuntos y superconjuntos
frozenset: el conjunto inmutable- Comprensiones de conjuntos
Crear conjuntos (y la trampa del {} vacío)
generos = {"novela", "teatro", "poesía"} # literal: llaves con elementos sueltos
desde_lista = set(["novela", "teatro", "novela"]) # set() convierte iterables y DEDUPLICA
print(desde_lista) # {'novela', 'teatro'} → el duplicado se fue
letras = set("papyrus") # {'p', 'a', 'y', 'r', 'u', 's'} → la 'p' repetida, fuera
vacio = set() # ÚNICA forma de crear un conjunto vacío
trampa = {} # ¡esto es un DICCIONARIO vacío, no un set!
print(type(vacio), type(trampa)) # <class 'set'> <class 'dict'>La trampa del {} es histórica: los diccionarios llegaron antes a Python y se quedaron con las llaves vacías. Memorízalo: conjunto vacío = set(), siempre.
Los elementos deben ser inmutables (hashables), como las claves de un dict: cadenas, números y tuplas sí; listas y diccionarios no. Y no hay indexación: generos[0] es un TypeError, porque un conjunto no tiene posiciones.
Unicidad: los duplicados desaparecen
La unicidad no es una restricción molesta: es la funcionalidad estrella. El caso de uso más frecuente del mundo real — eliminar duplicados de una lista — es una línea:
# Pedidos del día en Papyrus: Luis ha pedido El Quijote dos veces por error
pedidos = ["El Quijote", "Hamlet", "El Quijote", "Fausto", "Hamlet"]
titulos_unicos = set(pedidos)
print(titulos_unicos) # {'Fausto', 'Hamlet', 'El Quijote'} (orden no garantizado)
print(len(titulos_unicos)) # 3 títulos distintos, aunque hubo 5 pedidos
# Si necesitas conservar una lista (p. ej., ordenada), convierte de vuelta:
unicos_ordenados = sorted(set(pedidos))
print(unicos_ordenados) # ['El Quijote', 'Fausto', 'Hamlet']Cuidado: set() no conserva el orden original de la lista. Si el orden de aparición importa, hay un idiom con dict que verás en los ejercicios.
Añadir y eliminar elementos
Los conjuntos son mutables (existe versión inmutable: frozenset, más abajo):
generos = {"novela", "teatro"}
generos.add("poesía") # añade un elemento (si ya está, no pasa nada)
generos.add("teatro") # sin efecto ni error: ya estaba
generos.update(["ensayo", "novela"]) # añade varios de golpe (como extend en listas)
generos.remove("ensayo") # elimina; KeyError si no existe
generos.discard("ciencia") # elimina si está; SILENCIO si no está
elemento = generos.pop() # saca un elemento ARBITRARIO (no "el último": no hay orden)
generos.clear() # vacía el conjunto| Método | Si el elemento no está... |
|---|---|
remove(x) |
Lanza KeyError |
discard(x) |
No hace nada (la opción tranquila) |
Pertenencia ultrarrápida: por qué in vuela
Con una lista, x in lista obliga a Python a comparar elemento a elemento, desde el principio: si la lista tiene un millón de entradas, en el peor caso hace un millón de comparaciones. Con un conjunto (o las claves de un dict), Python calcula el hash del elemento — una huella numérica — y salta directamente a la casilla interna donde estaría si existiera. Una comprobación, tenga el conjunto diez elementos o diez millones.
socios = {"LUIS-001", "MARTA-002", "PAU-003"} # códigos de socio de Papyrus
def es_socio_valido(codigo):
"""La función de papyrus_utils, ahora respaldada por un set: validación inmediata."""
return codigo.strip().upper() in socios
print(es_socio_valido("luis-001")) # True
print(es_socio_valido("ANA-999")) # FalseEsta es también la respuesta a la pregunta de 04-03: catalogo["Fausto"] es instantáneo porque las claves de un diccionario usan exactamente el mismo mecanismo de hash. No necesitas la teoría formal de complejidad (llegará si algún día la buscas como "O(1) vs O(n)"); quédate con la regla práctica:
Muchas comprobaciones de "¿está o no está?" sobre la misma colección → conviértela en
setprimero.
Operaciones de conjuntos: unión, intersección, diferencias
Aquí los conjuntos brillan con luz propia. Cada operación existe como operador y como método (el método acepta cualquier iterable; el operador exige dos sets):
| Operación | Operador | Método | Resultado |
|---|---|---|---|
| Unión | a | b |
a.union(b) |
Elementos en a o en b (o ambos) |
| Intersección | a & b |
a.intersection(b) |
Elementos en a y en b |
| Diferencia | a - b |
a.difference(b) |
En a pero no en b |
| Diferencia simétrica | a ^ b |
a.symmetric_difference(b) |
En a o en b, pero no en ambos |
Con datos de Papyrus: Ana quiere analizar la fidelidad de sus socios comparando los compradores de junio y julio:
compraron_junio = {"Luis", "Marta", "Pau", "Júlia"}
compraron_julio = {"Luis", "Júlia", "Omar"}
print(compraron_junio | compraron_julio) # {'Luis','Marta','Pau','Júlia','Omar'} → clientela activa
print(compraron_junio & compraron_julio) # {'Luis', 'Júlia'} → los fieles: repiten mes a mes
print(compraron_junio - compraron_julio) # {'Marta', 'Pau'} → compraron en junio y julio NO: ¿recuperarlos?
print(compraron_julio - compraron_junio) # {'Omar'} → cliente nuevo en julio
print(compraron_junio ^ compraron_julio) # {'Marta','Pau','Omar'} → solo un mes de los dosCinco preguntas de negocio, cinco líneas, cero bucles. La diferencia no es conmutativa (a - b != b - a); unión, intersección y diferencia simétrica sí lo son.
Otro uso natural: los géneros de cada libro como conjuntos, para responder "¿qué libros comparten género?":
generos_libro = {
"La Odisea": {"clásico", "épica"},
"Hamlet": {"clásico", "teatro", "tragedia"},
"Fausto": {"clásico", "teatro"},
}
comunes = generos_libro["Hamlet"] & generos_libro["Fausto"]
print(comunes) # {'clásico', 'teatro'}Subconjuntos y superconjuntos
tragedias = {"Hamlet", "Fausto"}
clasicos = {"La Odisea", "Hamlet", "El Quijote", "Fausto"}
print(tragedias <= clasicos) # True: tragedias es SUBconjunto de clasicos
print(tragedias.issubset(clasicos)) # equivalente con método
print(clasicos >= tragedias) # True: clasicos es SUPERconjunto
print(tragedias.isdisjoint({"Ilíada"})) # True: no comparten ningún elementoÚtil para validaciones: "¿todos los títulos del pedido están en el catálogo?" es set(pedido) <= set(catalogo) — y sí, set(catalogo) sobre el dict de 04-03 extrae sus claves.
frozenset: el conjunto inmutable
La misma relación que tupla/lista: frozenset es un conjunto congelado. No admite add() ni remove(), y a cambio es hashable — puede ser clave de diccionario o elemento de otro conjunto:
PACK_CLASICOS = frozenset({"La Odisea", "Hamlet"}) # oferta fija de la tienda
ofertas = {
PACK_CLASICOS: 19.90, # un frozenset como clave de dict: legal
}
print(ofertas[frozenset({"Hamlet", "La Odisea"})]) # 19.9 → el orden no importaCon un set normal como clave obtendrías TypeError: unhashable type: 'set'. Uso breve y ocasional, pero cuando lo necesitas, nada más lo sustituye.
Comprensiones de conjuntos
Tercera comprensión de la familia (lista → dict → set). Sintaxis: llaves sin dos puntos:
pedidos = [" hamlet ", "FAUSTO", "Hamlet", "el quijote"]
# Títulos normalizados y únicos, de golpe (la normalización a fondo, en 04-05)
unicos = {p.strip().lower() for p in pedidos}
print(unicos) # {'hamlet', 'fausto', 'el quijote'} → 4 pedidos, 3 títulos reales
# Sobre el catálogo canónico de 04-03: títulos con stock
catalogo = {
"La Odisea": {"precio": 12.50, "stock": 4},
"Hamlet": {"precio": 9.95, "stock": 6},
"El Quijote": {"precio": 15.90, "stock": 8},
"Fausto": {"precio": 21.00, "stock": 0},
}
disponibles = {t for t, ficha in catalogo.items() if ficha["stock"] > 0}
print(disponibles) # {'La Odisea', 'Hamlet', 'El Quijote'}| Comprensión | Sintaxis | Produce |
|---|---|---|
| De lista | [x for x in it] |
Lista (ordenada, con duplicados) |
| De diccionario | {k: v for ...} |
Dict (clave: valor) |
| De conjunto | {x for x in it} |
Set (único, sin orden) |
Errores Comunes y Consejos
{}no es un conjunto vacío: es un dict. El error no da mensaje — simplementetrampa.add(x)fallará conAttributeErrormás tarde. Conjunto vacío =set().- Esperar orden: los sets no garantizan orden de iteración. Si imprimes un set y "sale desordenado", no es un bug. Para presentar,
sorted(mi_set). - Indexar un set:
s[0]lanzaTypeError. Sin orden no hay posiciones; si necesitas "el primero", te falta una lista. - Meter elementos mutables:
{["a", "b"]}lanzaTypeError: unhashable type: 'list'. Convierte a tupla si necesitas agrupar. - Confundir
remove()condiscard(): si la ausencia es normal,discard(); si es un bug,remove()(que falle pronto). El mismo criterio que corchetes vsget()en dicts. - Deduplicar con set cuando el orden importa:
set(lista)pierde el orden de aparición. Idiom que lo conserva:list(dict.fromkeys(lista))— los dicts deduplican claves y recuerdan el orden de inserción. - Consejo: si te sorprendes escribiendo bucles anidados para comparar dos listas ("los de aquí que no están allá"), casi seguro que era una operación de conjuntos.
Ejercicios
- Auditoría de pedidos. De la lista
pedidos = ["Hamlet", "Fausto", "Hamlet", "Drácula", "El Quijote", "Fausto"], obtén: (a) el conjunto de títulos distintos pedidos; (b) cuántos pedidos duplicados hubo (total de pedidos menos títulos distintos); (c) usando el catálogo canónico de 04-03 y una diferencia de conjuntos, qué títulos pedidos no están en el catálogo. - Campaña de fidelización. Con
junio = {"Luis", "Marta", "Pau", "Júlia"}yjulio = {"Luis", "Júlia", "Omar"}, calcula: los socios que hay que recuperar (compraron en junio pero no en julio), los nuevos de julio, y los fieles de ambos meses. Después comprueba con un operador de subconjunto si todos los fieles compraron en junio. - Géneros disponibles. Con el dict
generos_librode la lección (La Odisea, Hamlet, Fausto) y el conjuntodisponiblescalculado sobre el catálogo canónico, construye mediante operaciones y/o una comprensión el conjunto de géneros realmente disponibles hoy (los géneros de los libros con stock > 0). Pista: la unión de varios conjuntos puede hacerse conset().union(...).
Soluciones
# Ejercicio 1
pedidos = ["Hamlet", "Fausto", "Hamlet", "Drácula", "El Quijote", "Fausto"]
catalogo = {
"La Odisea": {"precio": 12.50, "stock": 4},
"Hamlet": {"precio": 9.95, "stock": 6},
"El Quijote": {"precio": 15.90, "stock": 8},
"Fausto": {"precio": 21.00, "stock": 0},
}
distintos = set(pedidos) # (a) {'Hamlet','Fausto','Drácula','El Quijote'}
duplicados = len(pedidos) - len(distintos) # (b) 2
desconocidos = distintos - set(catalogo) # (c) {'Drácula'} → set(dict) toma las claves
print(distintos, duplicados, desconocidos)# Ejercicio 2
junio = {"Luis", "Marta", "Pau", "Júlia"}
julio = {"Luis", "Júlia", "Omar"}
recuperar = junio - julio # {'Marta', 'Pau'}
nuevos = julio - junio # {'Omar'}
fieles = junio & julio # {'Luis', 'Júlia'}
print(fieles <= junio) # True: todo fiel compró en junio (por definición)Consejo: nombra los resultados con vocabulario de negocio (recuperar, fieles), no matemático (diferencia, interseccion): el código se explica solo.
# Ejercicio 3
generos_libro = {
"La Odisea": {"clásico", "épica"},
"Hamlet": {"clásico", "teatro", "tragedia"},
"Fausto": {"clásico", "teatro"},
}
disponibles = {t for t, ficha in catalogo.items() if ficha["stock"] > 0}
generos_hoy = set().union(*(generos_libro[t] for t in disponibles if t in generos_libro))
print(generos_hoy) # {'clásico', 'épica', 'teatro', 'tragedia'} → Fausto (agotado) no aportaFíjate: el * desempaqueta los conjuntos como argumentos de union() (el mismo * de 03-02), y el if t in generos_libro protege contra libros sin géneros fichados — "El Quijote" está disponible pero no tiene entrada, y sin ese filtro habría un KeyError.
Conclusión
Los conjuntos completan el trío de colecciones "con llaves": sabes crearlos (recordando que set() es la única forma del vacío), aprovechar la deduplicación automática, y expresar lógica de negocio con unión, intersección y diferencias en una línea donde antes habrías anidado bucles. Entiendes además por qué in es ultrarrápido en sets y dicts — la magia del hash — y conoces frozenset para cuando la inmutabilidad importa. En Papyrus ya deduplican pedidos, validan socios y comparan meses de ventas. Pero en casi todos los ejemplos ha asomado un actor que llevamos usando desde el módulo 1 sin estudiarlo en serio: la cadena de texto, con sus strip(), lower() y compañía. La próxima lección la trata por fin como lo que es — una secuencia con superpoderes propios — y sistematiza la normalización de entradas de usuario que buscar_libro() y es_socio_valido() llevan haciendo a medias.
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
