Saltar al contenido
🐍 Curso de Python
/

Módulo 1 · Lección 27 de 59

Conjuntos: valores sin repetir

La colección que no admite duplicados ni guarda orden, cómo limpiar repetidos y comparar dos grupos de valores.

⏱ 12 min de lectura

Tienes una lista con los correos de los asistentes a un curso y algunos se han apuntado dos veces. Quieres la lista de correos únicos. Con lo que sabes, la solución pasa por recorrer, comprobar con in si ya estaba y hacer append solo si no. Funciona, pero es mucho código para algo tan simple.

Tienes otra lista con los asistentes del año pasado y quieres saber quiénes repiten. Y quiénes son nuevos. Y quiénes se han ido. Con listas y bucles, tres bloques de código más.

Un conjunto (set) resuelve todo eso de una línea cada cosa. Es una colección con dos reglas: no admite elementos repetidos y no guarda el orden.

Crear un conjunto#

Se escribe con llaves:

python
colores = {"rojo", "verde", "azul"}

print(len(colores))
print(type(colores))
print("verde" in colores)
Salida en pantalla
3
<class 'set'>
True

Los duplicados desaparecen solos, sin avisar y sin dar error:

python
votos = {"sí", "no", "sí", "sí", "no"}

print(len(votos))
print(sorted(votos))
Salida en pantalla
2
['no', 'sí']

El conjunto vacío no se escribe con llaves#

Aquí hay un detalle que hay que memorizar, porque las llaves estaban pilladas de antes por los diccionarios:

python
no_es_conjunto = {}
si_es_conjunto = set()

print(type(no_es_conjunto))
print(type(si_es_conjunto))
Salida en pantalla
<class 'dict'>
<class 'set'>

{} es un diccionario vacío. Para un conjunto vacío hay que escribir set(). Es una decisión histórica: los diccionarios llegaron a Python muchos años antes que los conjuntos y se quedaron con la notación.

Su uso más frecuente: quitar duplicados#

Esta es la razón por la que la mayoría de la gente usa conjuntos, y merece la pena que la reconozcas de un vistazo:

python
correos = [
    "ana@correo.es",
    "luis@correo.es",
    "ana@correo.es",
    "marta@correo.es",
    "luis@correo.es",
]

unicos = list(set(correos))

print(f"Recibidos: {len(correos)}")
print(f"Únicos:    {len(unicos)}")
print(sorted(unicos))
Salida en pantalla
Recibidos: 5
Únicos:    3
['ana@correo.es', 'luis@correo.es', 'marta@correo.es']

set(correos) convierte la lista en conjunto, y al hacerlo tira los repetidos; list() la devuelve a lista. Todo en una expresión.

set() acepta cualquier cosa recorrible, incluidas las cadenas:

python
frase = "el conjunto de las letras"

letras = set(frase)
letras.discard(" ")

print(f"Caracteres distintos: {len(letras)}")
print("".join(sorted(letras)))
Salida en pantalla
Caracteres distintos: 12
acdejlnorstu

Añadir y quitar#

Un conjunto sí es mutable, aunque no tenga orden:

python
etiquetas = {"python", "curso"}

etiquetas.add("listas")
etiquetas.add("python")        # ya estaba: no pasa nada

print(sorted(etiquetas))

etiquetas.discard("curso")     # si no está, no hace nada
etiquetas.discard("django")    # tampoco falla

print(sorted(etiquetas))
Salida en pantalla
['curso', 'listas', 'python']
['listas', 'python']

Fíjate en que add se llama en singular y añade un elemento, y que volver a añadir algo que ya estaba no es un error: simplemente no cambia nada. Esa tolerancia es justo lo que hace cómodos a los conjuntos.

Para quitar hay dos métodos que se diferencian en cómo reaccionan cuando el elemento no está:

python
etiquetas = {"python", "curso"}

etiquetas.remove("django")
Salida en pantalla
KeyError: 'django'
MétodoSi el elemento no está
conjunto.add(x)No aplica: añade o deja igual
conjunto.discard(x)No hace nada
conjunto.remove(x)KeyError
conjunto.pop()Saca uno cualquiera; KeyError si está vacío
conjunto.clear()Deja el conjunto vacío

pop() en un conjunto no recibe índice, porque no hay posiciones: saca un elemento arbitrario. Se usa poco, y nunca para "el primero".

Las operaciones de conjuntos#

Aquí es donde los conjuntos dejan de ser una lista sin repetidos y se convierten en una herramienta. Las cuatro operaciones tienen un operador y un método equivalentes.

python
del_ano_pasado = {"ana", "luis", "marta", "pedro"}
de_este_ano = {"luis", "marta", "sara", "jorge"}

print(f"Todos:              {sorted(del_ano_pasado | de_este_ano)}")
print(f"Repiten:            {sorted(del_ano_pasado & de_este_ano)}")
print(f"Solo el año pasado: {sorted(del_ano_pasado - de_este_ano)}")
print(f"Solo uno de los dos:{sorted(del_ano_pasado ^ de_este_ano)}")
Salida en pantalla
Todos:              ['ana', 'jorge', 'luis', 'marta', 'pedro', 'sara']
Repiten:            ['luis', 'marta']
Solo el año pasado: ['ana', 'pedro']
Solo uno de los dos:['ana', 'jorge', 'pedro', 'sara']
OperaciónOperadorMétodoQué devuelve
Unión`a \b`a.union(b)Los que están en alguno de los dos
Interseccióna & ba.intersection(b)Los que están en los dos
Diferenciaa - ba.difference(b)Los de a que no están en b
Diferencia simétricaa ^ ba.symmetric_difference(b)Los que están en uno pero no en los dos

Los métodos hacen exactamente lo mismo que los operadores:

python
del_ano_pasado = {"ana", "luis", "marta", "pedro"}
de_este_ano = {"luis", "marta", "sara", "jorge"}

print(sorted(del_ano_pasado.intersection(de_este_ano)))
print(sorted(del_ano_pasado.difference(de_este_ano)))
print(del_ano_pasado.intersection(de_este_ano) == del_ano_pasado & de_este_ano)
Salida en pantalla
['luis', 'marta']
['ana', 'pedro']
True

Usa los operadores cuando los dos lados sean conjuntos, porque se leen mejor, y los métodos cuando el otro lado sea una lista: a.union(lista) funciona, pero a | lista da TypeError.

La diferencia no es simétrica, y conviene tenerlo claro:

python
del_ano_pasado = {"ana", "luis", "marta", "pedro"}
de_este_ano = {"luis", "marta", "sara", "jorge"}

print(f"se fueron: {sorted(del_ano_pasado - de_este_ano)}")
print(f"nuevos:    {sorted(de_este_ano - del_ano_pasado)}")
Salida en pantalla
se fueron: ['ana', 'pedro']
nuevos:    ['jorge', 'sara']

Y hay tres comprobaciones más que devuelven True o False:

python
permisos_usuario = {"leer", "escribir"}
permisos_necesarios = {"leer"}

print(permisos_necesarios <= permisos_usuario)      # ¿está contenido?
print(permisos_usuario <= permisos_necesarios)
print({"admin"}.isdisjoint(permisos_usuario))       # ¿no comparten nada?
Salida en pantalla
True
False
True

a <= b pregunta si todos los elementos de a están en b, y también se escribe a.issubset(b). Es la forma directa de comprobar permisos, requisitos o etiquetas obligatorias.

Lo que un conjunto no puede contener#

Los elementos de un conjunto tienen que ser inmutables, por el mismo motivo que las claves de un diccionario: el conjunto calcula una especie de huella de cada valor para encontrarlo al instante, y si el valor cambiara por dentro esa huella dejaría de servir.

python
coordenadas = {(0, 0), (1, 2), (0, 0)}

print(len(coordenadas))
print((1, 2) in coordenadas)
Salida en pantalla
2
True

Las tuplas valen, como ves. Las listas no:

python
coordenadas = {[0, 0], [1, 2]}
Salida en pantalla
TypeError: cannot use 'list' as a set element (unhashable type: 'list')

Números, cadenas, booleanos y tuplas se pueden meter en un conjunto. Listas, diccionarios y otros conjuntos, no.

Por qué in es mucho más rápido en un conjunto#

Esto es lo que justifica que los conjuntos existan aunque ya tengas listas. Cuando preguntas "ana" in lista, Python no tiene más remedio que ir comparando elemento por elemento desde el principio. Si la lista tiene un millón de elementos y el que buscas está al final, hace un millón de comparaciones.

Un conjunto no busca: calcula. A partir del valor obtiene directamente en qué hueco de su tabla interna debería estar y mira solo ahí. Da lo mismo que el conjunto tenga diez elementos o diez millones: el número de comprobaciones es prácticamente el mismo.

busqueda.py
import time

numeros_lista = list(range(1000000))
numeros_conjunto = set(numeros_lista)
buscado = 999999

inicio = time.perf_counter()
for intento in range(100):
        buscado in numeros_lista
print(f"lista:    {time.perf_counter() - inicio:.4f} s")

inicio = time.perf_counter()
for intento in range(100):
        buscado in numeros_conjunto
print(f"conjunto: {time.perf_counter() - inicio:.4f} s")

Las cifras exactas dependen de tu ordenador, así que aquí no las pego: ejecútalo tú. En la máquina en la que se escribió esta lección, las cien búsquedas en la lista tardaron unas nueve décimas de segundo y las cien búsquedas en el conjunto salieron en 0.0000 s, es decir, por debajo de la precisión con la que las estamos midiendo. La diferencia es de varios órdenes de magnitud, no de un porcentaje.

Conjuntos por comprensión#

Funcionan igual que las listas por comprensión, cambiando los corchetes por llaves, y además quitan duplicados por el camino:

python
palabras = ["Python", "listas", "python", "Listas", "tuplas"]

distintas = {palabra.lower() for palabra in palabras}

print(len(distintas))
print(sorted(distintas))
Salida en pantalla
3
['listas', 'python', 'tuplas']

Tabla comparativa de las tres colecciones#

ListaTuplaConjunto
Se escribe[1, 2](1, 2){1, 2}
Vacía[]()set()
OrdenSíSíNo
DuplicadosSíSíNo
Acceso por índiceSíSíNo
Se puede modificarSíNoSí
Velocidad de inLentaLentaMuy rápida
Para quéDatos en orden que cambianDatos fijos que van juntosPertenencia y valores únicos

Un ejemplo completo#

asistentes.py
inscritos = ["ana", "luis", "marta", "pedro", "ana", "sara", "luis"]
asistieron = ["luis", "marta", "sara", "jorge"]

conjunto_inscritos = set(inscritos)
conjunto_asistieron = set(asistieron)

print(f"Inscripciones recibidas: {len(inscritos)}")
print(f"Personas distintas:      {len(conjunto_inscritos)}")
print(f"Duplicadas:              {len(inscritos) - len(conjunto_inscritos)}")
print()
print(f"Asistieron y estaban:    {sorted(conjunto_inscritos & conjunto_asistieron)}")
print(f"Faltaron:                {sorted(conjunto_inscritos - conjunto_asistieron)}")
print(f"Vinieron sin inscribirse:{sorted(conjunto_asistieron - conjunto_inscritos)}")
print()
porcentaje = len(conjunto_inscritos & conjunto_asistieron) / len(conjunto_inscritos) * 100
print(f"Asistencia: {porcentaje:.1f} %")
Salida en pantalla
Inscripciones recibidas: 7
Personas distintas:      5
Duplicadas:              2

Asistieron y estaban:    ['luis', 'marta', 'sara']
Faltaron:                ['ana', 'pedro']
Vinieron sin inscribirse:['jorge']

Asistencia: 60.0 %

Doce líneas de lógica para un informe que con listas y bucles ocuparía tres veces más y se leería peor.