Conjuntos (Set)
Conjuntos (Sets) en Python
Qué es un conjunto
Un conjunto es una colección no ordenada, mutable y sin elementos duplicados. Está inspirado en el concepto matemático de conjunto. Sus tres características fundamentales son que no permite elementos repetidos, no tiene un orden definido por lo que no se puede acceder por índice, y es mutable por lo que puedes añadir y eliminar elementos.
# Un conjunto elimina duplicados automáticamente
numeros = {1, 2, 2, 3, 3, 3, 4}
print(numeros) # {1, 2, 3, 4} ← sin duplicados
# No tiene orden garantizado
letras = {"c", "a", "b"}
print(letras) # puede imprimir {'a', 'b', 'c'} o en cualquier otro orden
# No se puede acceder por índice
try:
print(letras[0]) # TypeError: 'set' object is not subscriptable
except TypeError:
print("Los sets no tienen índices")
Cómo crear conjuntos
# Con llaves
colores = {"rojo", "verde", "azul"}
# Con set()
desde_lista = set([1, 2, 3, 2, 1]) # {1, 2, 3}
desde_string = set("mississippi") # {'m', 'i', 's', 'p'}
desde_tupla = set((1, 2, 3, 2)) # {1, 2, 3}
desde_rango = set(range(5)) # {0, 1, 2, 3, 4}
# Conjunto vacío — IMPORTANTE: {} crea un dict, no un set
vacio_dict = {} # dict vacío
vacio_set = set() # set vacío ← única forma correcta
print(type(vacio_dict)) # <class 'dict'>
print(type(vacio_set)) # <class 'set'>
# Con comprensión de conjunto
cuadrados = {n**2 for n in range(1, 6)}
print(cuadrados) # {1, 4, 9, 16, 25}
pares = {n for n in range(20) if n % 2 == 0}
print(pares) # {0, 2, 4, 6, 8, 10, 12, 14, 16, 18}
Qué tipos pueden ser elementos
Los elementos de un conjunto deben ser inmutables (hasheables). Las listas y diccionarios no pueden ser elementos de un conjunto, pero las tuplas sí.
# Tipos permitidos
valido = {1, 3.14, "hola", True, (1, 2, 3)}
print(valido) # {1, 3.14, 'hola', (1, 2, 3)}
# True y 1 son iguales para el set
raro = {True, 1, 2}
print(raro) # {True, 2} ← True y 1 se consideran el mismo elemento
# Tipos NO permitidos
try:
invalido = {[1, 2], [3, 4]} # lista no es hasheable
except TypeError:
print("Las listas no pueden ser elementos de un set")
Métodos para añadir elementos
.add() — añade un elemento
frutas = {"manzana", "pera"}
frutas.add("naranja")
print(frutas) # {'manzana', 'pera', 'naranja'}
# Añadir un duplicado no tiene efecto
frutas.add("manzana")
print(frutas) # {'manzana', 'pera', 'naranja'} ← sin cambios
print(len(frutas)) # 3 ← sigue siendo 3
# Uso práctico: registrar visitas únicas
visitas = set()
usuarios = ["ana", "carlos", "ana", "maría", "carlos", "ana"]
for usuario in usuarios:
visitas.add(usuario)
print(f"Visitantes únicos: {len(visitas)}") # 3
print(visitas) # {'ana', 'carlos', 'maría'}
.update() — añade varios elementos de otro iterable
colores = {"rojo", "verde"}
# Desde otro set
colores.update({"azul", "amarillo"})
print(colores) # {'rojo', 'verde', 'azul', 'amarillo'}
# Desde lista
colores.update(["naranja", "morado"])
print(colores)
# Desde string — añade cada carácter
letras = {"a", "b"}
letras.update("cd")
print(letras) # {'a', 'b', 'c', 'd'}
# Desde varios iterables a la vez
base = {1, 2}
base.update([3, 4], (5, 6), {7, 8})
print(base) # {1, 2, 3, 4, 5, 6, 7, 8}
Métodos para eliminar elementos
.remove() — elimina un elemento, error si no existe
frutas = {"manzana", "pera", "naranja"}
frutas.remove("pera")
print(frutas) # {'manzana', 'naranja'}
# Si no existe lanza KeyError
try:
frutas.remove("kiwi")
except KeyError:
print("kiwi no está en el conjunto")
.discard() — elimina un elemento sin error si no existe
frutas = {"manzana", "pera", "naranja"}
frutas.discard("pera") # existe → lo elimina
frutas.discard("kiwi") # no existe → no hace nada, sin error
print(frutas) # {'manzana', 'naranja'}
# Uso práctico: eliminar si existe sin comprobación previa
def limpiar_etiquetas(etiquetas, no_deseadas):
for etiqueta in no_deseadas:
etiquetas.discard(etiqueta)
return etiquetas
etiquetas = {"python", "programación", "spam", "tutorial", "basura"}
no_deseadas = {"spam", "basura", "publicidad"}
limpiar_etiquetas(etiquetas, no_deseadas)
print(etiquetas) # {'python', 'programación', 'tutorial'}
.pop() — elimina y devuelve un elemento aleatorio
numeros = {1, 2, 3, 4, 5}
elemento = numeros.pop() # elimina uno cualquiera
print(elemento) # por ejemplo: 1
print(numeros) # el resto sin ese elemento
# Si el conjunto está vacío lanza KeyError
vacio = set()
try:
vacio.pop()
except KeyError:
print("No se puede hacer pop en un conjunto vacío")
.clear() — vacía el conjunto
colores = {"rojo", "verde", "azul"}
colores.clear()
print(colores) # set()
Operaciones matemáticas de conjuntos
Esta es la característica más poderosa de los sets. Permiten realizar operaciones de teoría de conjuntos de forma muy elegante.
Unión — todos los elementos de ambos
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
# Con método
print(a.union(b)) # {1, 2, 3, 4, 5, 6}
# Con operador |
print(a | b) # {1, 2, 3, 4, 5, 6}
# Con varios conjuntos
c = {6, 7, 8}
print(a | b | c) # {1, 2, 3, 4, 5, 6, 7, 8}
print(a.union(b, c)) # {1, 2, 3, 4, 5, 6, 7, 8}
# Uso práctico
alumnos_mañana = {"Ana", "Carlos", "María"}
alumnos_tarde = {"Pedro", "Carlos", "Lucía"}
todos_alumnos = alumnos_mañana | alumnos_tarde
print(todos_alumnos)
# {'Ana', 'Carlos', 'María', 'Pedro', 'Lucía'}
Intersección — solo los elementos comunes
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
# Con método
print(a.intersection(b)) # {3, 4}
# Con operador &
print(a & b) # {3, 4}
# Con varios conjuntos
c = {4, 5, 6, 7}
print(a & b & c) # {4}
# Uso práctico
asignaturas_ana = {"Matemáticas", "Física", "Historia", "Lengua"}
asignaturas_carlos = {"Física", "Química", "Historia", "Arte"}
en_comun = asignaturas_ana & asignaturas_carlos
print(f"Asignaturas en común: {en_comun}")
# Asignaturas en común: {'Física', 'Historia'}
Diferencia — elementos del primero que no están en el segundo
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
# Con método
print(a.difference(b)) # {1, 2} ← en a pero no en b
print(b.difference(a)) # {5, 6} ← en b pero no en a
# Con operador -
print(a - b) # {1, 2}
print(b - a) # {5, 6}
# Uso práctico
alumnos_matriculados = {"Ana", "Carlos", "María", "Pedro", "Lucía"}
alumnos_presentados = {"Ana", "María", "Lucía"}
no_presentados = alumnos_matriculados - alumnos_presentados
print(f"No se presentaron: {no_presentados}")
# No se presentaron: {'Carlos', 'Pedro'}
Diferencia simétrica — elementos que están en uno u otro pero no en ambos
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
# Con método
print(a.symmetric_difference(b)) # {1, 2, 5, 6}
# Con operador ^
print(a ^ b) # {1, 2, 5, 6}
# Uso práctico
inventario_ayer = {"laptop", "ratón", "teclado", "monitor"}
inventario_hoy = {"laptop", "ratón", "auriculares", "webcam"}
cambios = inventario_ayer ^ inventario_hoy
print(f"Productos que cambiaron: {cambios}")
# Productos que cambiaron: {'teclado', 'monitor', 'auriculares', 'webcam'}
# Separar los añadidos de los eliminados
añadidos = inventario_hoy - inventario_ayer
eliminados = inventario_ayer - inventario_hoy
print(f"Añadidos: {añadidos}") # {'auriculares', 'webcam'}
print(f"Eliminados: {eliminados}") # {'teclado', 'monitor'}
Operadores de asignación aumentada
Modifican el conjunto en sitio sin crear uno nuevo.
a = {1, 2, 3}
a |= {4, 5} # unión en sitio
print(a) # {1, 2, 3, 4, 5}
a &= {2, 3, 4} # intersección en sitio
print(a) # {2, 3, 4}
a -= {3} # diferencia en sitio
print(a) # {2, 4}
a ^= {1, 2} # diferencia simétrica en sitio
print(a) # {1, 4}
Métodos de comparación
.issubset() — ¿es subconjunto?
a = {1, 2}
b = {1, 2, 3, 4}
print(a.issubset(b)) # True ← todos los de a están en b
print(b.issubset(a)) # False
# Con operador <=
print(a <= b) # True
print(a < b) # True ← subconjunto estricto (a != b)
print(a <= a) # True ← un conjunto es subconjunto de sí mismo
print(a < a) # False ← no es subconjunto estricto de sí mismo
# Uso práctico
permisos_necesarios = {"leer", "escribir"}
permisos_usuario = {"leer", "escribir", "ejecutar", "borrar"}
tiene_acceso = permisos_necesarios <= permisos_usuario
print(f"Acceso concedido: {tiene_acceso}") # True
.issuperset() — ¿es superconjunto?
a = {1, 2, 3, 4}
b = {1, 2}
print(a.issuperset(b)) # True ← a contiene todos los de b
print(b.issuperset(a)) # False
# Con operador >=
print(a >= b) # True
print(a > b) # True ← superconjunto estricto
# Uso práctico
ingredientes_disponibles = {"harina", "azúcar", "huevos", "leche", "mantequilla"}
ingredientes_receta = {"harina", "azúcar", "huevos"}
puedo_hacer = ingredientes_disponibles >= ingredientes_receta
print(f"Puedo hacer la receta: {puedo_hacer}") # True
.isdisjoint() — ¿no tienen elementos en común?
a = {1, 2, 3}
b = {4, 5, 6}
c = {3, 4, 5}
print(a.isdisjoint(b)) # True ← no comparten ningún elemento
print(a.isdisjoint(c)) # False ← comparten el 3
# Uso práctico
dias_ana = {"lunes", "miércoles", "viernes"}
dias_carlos = {"martes", "jueves", "sábado"}
sin_conflicto = dias_ana.isdisjoint(dias_carlos)
print(f"Sin conflicto de horario: {sin_conflicto}") # True
Frozenset — conjunto inmutable
Un frozenset es como un set pero inmutable. No se puede modificar después de crearse. Esto permite usarlo como clave de diccionario o elemento de otro set.
# Crear frozenset
fs = frozenset([1, 2, 3, 4])
print(fs) # frozenset({1, 2, 3, 4})
# No se puede modificar
try:
fs.add(5)
except AttributeError:
print("frozenset no tiene método add")
# Sí soporta operaciones de conjuntos (devuelven frozenset nuevos)
a = frozenset({1, 2, 3})
b = frozenset({3, 4, 5})
print(a | b) # frozenset({1, 2, 3, 4, 5})
print(a & b) # frozenset({3})
# Se puede usar como clave de diccionario
permisos = {
frozenset({"leer"}): "rol_lector",
frozenset({"leer", "escribir"}): "rol_editor",
frozenset({"leer", "escribir", "borrar"}): "rol_admin",
}
mis_permisos = frozenset({"leer", "escribir"})
print(permisos[mis_permisos]) # rol_editor
# Se puede usar como elemento de otro set
grupos = {frozenset({1, 2}), frozenset({3, 4}), frozenset({1, 2})}
print(grupos) # {frozenset({1, 2}), frozenset({3, 4})} ← sin duplicados
Comprobaciones
frutas = {"manzana", "pera", "naranja"}
# Pertenencia
print("pera" in frutas) # True
print("kiwi" in frutas) # False
print("kiwi" not in frutas) # True
# Tamaño
print(len(frutas)) # 3
# Vacío
print(bool(set())) # False
print(bool(frutas)) # True
if not set():
print("El conjunto está vacío")
Cuándo usar sets
# 1. Eliminar duplicados de una lista
notas_con_duplicados = [8.5, 6.0, 8.5, 9.2, 6.0, 7.5, 8.5]
notas_unicas = list(set(notas_con_duplicados))
print(notas_unicas) # [8.5, 9.2, 6.0, 7.5] ← orden no garantizado
# Si necesitas mantener el orden
def sin_duplicados_ordenado(lista):
vistos = set()
return [x for x in lista if not (x in vistos or vistos.add(x))]
print(sin_duplicados_ordenado(notas_con_duplicados))
# [8.5, 6.0, 9.2, 7.5] ← orden original preservado
# 2. Comprobación de pertenencia rápida
# Los sets usan hashing, son O(1) mientras que las listas son O(n)
palabras_prohibidas = {"spam", "basura", "publicidad", "oferta"}
mensaje = "esto es publicidad barata"
tiene_spam = any(palabra in palabras_prohibidas
for palabra in mensaje.split())
print(f"Mensaje sospechoso: {tiene_spam}") # True
# 3. Operaciones entre colecciones
compradores_enero = {"Ana", "Carlos", "María", "Pedro"}
compradores_febrero = {"María", "Pedro", "Lucía", "Juan"}
# Clientes que compraron ambos meses
fieles = compradores_enero & compradores_febrero
print(f"Clientes fieles: {fieles}")
# Clientes nuevos en febrero
nuevos = compradores_febrero - compradores_enero
print(f"Clientes nuevos: {nuevos}")
# Clientes perdidos en febrero
perdidos = compradores_enero - compradores_febrero
print(f"Clientes perdidos: {perdidos}")
# Total de clientes únicos
todos = compradores_enero | compradores_febrero
print(f"Total únicos: {todos}")
Ejemplo práctico completo — sistema de etiquetas y permisos
class SistemaPermisos:
PERMISOS_DISPONIBLES = frozenset({
"leer", "escribir", "ejecutar",
"borrar", "administrar", "compartir"
})
def __init__(self):
self.usuarios = {}
def crear_usuario(self, nombre, permisos=None):
if permisos:
invalidos = set(permisos) - self.PERMISOS_DISPONIBLES
if invalidos:
print(f"Permisos no válidos: {invalidos}")
return False
self.usuarios[nombre] = set(permisos) if permisos else set()
print(f"✓ Usuario '{nombre}' creado con permisos: "
f"{self.usuarios[nombre] or 'ninguno'}")
return True
def conceder_permiso(self, nombre, permiso):
if nombre not in self.usuarios:
print(f"Usuario '{nombre}' no existe")
return False
if permiso not in self.PERMISOS_DISPONIBLES:
print(f"Permiso '{permiso}' no válido")
return False
self.usuarios[nombre].add(permiso)
print(f"✓ Permiso '{permiso}' concedido a '{nombre}'")
return True
def revocar_permiso(self, nombre, permiso):
if nombre not in self.usuarios:
print(f"Usuario '{nombre}' no existe")
return False
self.usuarios[nombre].discard(permiso)
print(f"✓ Permiso '{permiso}' revocado de '{nombre}'")
return True
def tiene_permiso(self, nombre, permiso):
return permiso in self.usuarios.get(nombre, set())
def tiene_permisos(self, nombre, permisos_requeridos):
return set(permisos_requeridos) <= self.usuarios.get(nombre, set())
def permisos_en_comun(self, nombre1, nombre2):
p1 = self.usuarios.get(nombre1, set())
p2 = self.usuarios.get(nombre2, set())
return p1 & p2
def usuarios_con_permiso(self, permiso):
return {nombre for nombre, perms in self.usuarios.items()
if permiso in perms}
def mostrar_informe(self):
print(f"\n{'='*50}")
print(" INFORME DE PERMISOS")
print(f"{'='*50}")
for nombre, permisos in sorted(self.usuarios.items()):
print(f"\n {nombre}:")
if permisos:
for p in sorted(permisos):
print(f" ✓ {p}")
else:
print(" (sin permisos)")
print(f"\n Resumen por permiso:")
for permiso in sorted(self.PERMISOS_DISPONIBLES):
usuarios = self.usuarios_con_permiso(permiso)
print(f" {permiso:<12} → {len(usuarios)} usuario(s)")
print(f"{'='*50}")
# Uso del sistema
sistema = SistemaPermisos()
sistema.crear_usuario("ana", ["leer", "escribir", "compartir"])
sistema.crear_usuario("carlos", ["leer"])
sistema.crear_usuario("admin", ["leer", "escribir", "ejecutar",
"borrar", "administrar", "compartir"])
sistema.crear_usuario("invitado")
sistema.conceder_permiso("carlos", "escribir")
sistema.revocar_permiso("ana", "compartir")
print(f"\n¿Ana puede borrar? {sistema.tiene_permiso('ana', 'borrar')}")
print(f"¿Admin puede borrar? {sistema.tiene_permiso('admin', 'borrar')}")
print(f"\n¿Carlos puede leer y escribir? "
f"{sistema.tiene_permisos('carlos', ['leer', 'escribir'])}")
comunes = sistema.permisos_en_comun("ana", "carlos")
print(f"\nPermisos en común (Ana y Carlos): {comunes}")
pueden_borrar = sistema.usuarios_con_permiso("borrar")
print(f"Pueden borrar: {pueden_borrar}")
sistema.mostrar_informe()
```
Salida:
```
✓ Usuario 'ana' creado con permisos: {'leer', 'escribir', 'compartir'}
✓ Usuario 'carlos' creado con permisos: {'leer'}
✓ Usuario 'admin' creado con permisos: {'leer', 'escribir', 'ejecutar', 'borrar', 'administrar', 'compartir'}
✓ Usuario 'invitado' creado con permisos: ninguno
✓ Permiso 'escribir' concedido a 'carlos'
✓ Permiso 'compartir' revocado de 'ana'
¿Ana puede borrar? False
¿Admin puede borrar? True
¿Carlos puede leer y escribir? True
Permisos en común (Ana y Carlos): {'leer', 'escribir'}
Pueden borrar: {'admin'}
==================================================
INFORME DE PERMISOS
==================================================
admin:
✓ administrar
✓ borrar
✓ compartir
✓ ejecutar
✓ escribir
✓ leer
ana:
✓ escribir
✓ leer
carlos:
✓ escribir
✓ leer
invitado:
(sin permisos)
Resumen por permiso:
administrar → 1 usuario(s)
borrar → 1 usuario(s)
compartir → 1 usuario(s)
ejecutar → 1 usuario(s)
escribir → 3 usuario(s)
leer → 3 usuario(s)
==================================================
Resumen de métodos
| Método | Operador | Qué hace | Modifica original |
|---|---|---|---|
.add(x) |
— | Añade un elemento | Sí |
.update(iter) |
|= |
Añade varios elementos | Sí |
.remove(x) |
— | Elimina x, error si no existe | Sí |
.discard(x) |
— | Elimina x sin error | Sí |
.pop() |
— | Elimina y devuelve uno aleatorio | Sí |
.clear() |
— | Vacía el conjunto | Sí |
.union(s) |
| |
Unión | No |
.intersection(s) |
& |
Intersección | No |
.difference(s) |
- |
Diferencia | No |
.symmetric_difference(s) |
^ |
Diferencia simétrica | No |
.issubset(s) |
<= |
¿Es subconjunto? | No |
.issuperset(s) |
>= |
¿Es superconjunto? | No |
.isdisjoint(s) |
— | ¿Sin elementos comunes? | No |
.copy() |
— | Copia superficial | No |