Conjuntos — una colección sin duplicados
Quitar duplicados, los cuatro símbolos que comparan dos colecciones, por qué {} no es un conjunto vacío y qué significa de verdad no tener orden.
¿Cómo quieres aprender?
El problema que vamos a resolver
Las ventas de un día llegan como una lista, una entrada por venta:
sold = ["pen", "bag", "pen", "ink", "pen"]La pregunta: ¿cuántos productos distintos se vendieron? len(sold) dice cinco, pero ese es el número de ventas. Hubo tres productos distintos.
Los capítulos anteriores pueden con eso — empieza con una lista vacía, y para cada elemento comprueba if item not in unique: antes de añadirlo. Seis líneas, y funciona.
Pero si la pregunta es «¿qué productos se vendieron tanto el lunes como el martes?», necesitas dos bucles anidados. «¿Solo el lunes?» — otro. Estas preguntas son lo bastante comunes como para tener su propia rama de las matemáticas, y en Python esas matemáticas se pueden escribir directamente.
Un conjunto es el contenedor para eso: no guarda duplicados, y responde «en ambos», «en uno pero no en el otro» con un solo carácter.
Al terminar este capítulo podrás
- Crear un conjunto y quitar los duplicados de una lista
- Comparar dos conjuntos con
|,&,-y^ - Explicar por qué
{}no es un conjunto vacío - Saber que un conjunto no tiene orden, y cuándo eso importa
- Escribir el patrón que quita los duplicados conservando el orden
Requisitos previos: Diccionarios — pares de clave y valor.
Un conjunto — una colección sin duplicados
sold = ["pen", "bag", "pen", "ink", "pen"]
unique = set(sold)
print(sorted(unique))
print(len(unique))['bag', 'ink', 'pen']
3set(...) toma una lista y descarta los duplicados. Los tres pen se convierten en uno.
Fíjate en el sorted() al imprimir, que es intencionado — la razón llega enseguida.
También puedes escribir uno directamente, con llaves:
letters = {"a", "b", "a"}
print(sorted(letters))
print(len(letters))['a', 'b']
2El segundo "a" desapareció en silencio. Sin error — un conjunto no tiene noción de que la misma cosa aparezca dos veces.
{} no es un conjunto vacío
not_a_set = {}
real_set = set()
print(type(not_a_set))
print(type(real_set))<class 'dict'>
<class 'set'>Las llaves se usaron primero para los diccionarios, así que un {} vacío siguió siendo un diccionario. Para un conjunto vacío hay que escribir set().
El error pasa inadvertido al principio: empieza con {}, llama a .add(), y recibes un AttributeError que hace parecer que add es el problema.
No hay orden
Esta es la limitación más importante de un conjunto, y la propiedad que más se malinterpreta.
Las cosas de un conjunto no tienen orden. No hay índices ni rebanadas:
tags = {"new", "sale"}
print(tags[0])TypeError: 'set' object is not subscriptableY el orden que ves al imprimir un conjunto no es fiable — ejecuta el mismo programa dos veces y puede ser distinto. Por eso todos los ejemplos de este capítulo ordenan antes de imprimir: lo que ves al imprimir un conjunto directamente no está garantizado que sea lo que veas la próxima vez.
La forma más corta de quitar los duplicados de una lista es list(set(sold)), y tira el orden original. Cuando el orden importa, usa un conjunto y una lista juntos:
sold = ["pen", "bag", "pen", "ink", "bag"]
seen = set()
unique = []
for item in sold:
if item not in seen:
seen.add(item)
unique.append(item)
print(unique)['pen', 'bag', 'ink']La lista conserva el orden y el conjunto recuerda lo que ya se ha visto. Esa combinación es un patrón muy común.
Añadir y quitar
tags = {"new"}
tags.add("sale")
tags.add("new")
print(sorted(tags))
tags.discard("gone")
tags.remove("sale")
print(sorted(tags))['new', 'sale']
['new']add mete una cosa — y si ya está ahí, no pasa nada y nadie se queja. El append de una lista habría hecho una segunda copia; un conjunto no.
Quitar tiene dos formas, y la diferencia es útil:
tags = {"new"}
tags.remove("gone")KeyError: 'gone'remove se detiene cuando no encuentra la cosa; discard no hace nada en silencio. Si la cosa debería estar ahí, escribe remove para que su ausencia se detecte.
Comparar dos conjuntos
Aquí es donde los conjuntos son realmente útiles:
monday = {"rafi", "ahmed", "bilal"}
tuesday = {"ahmed", "dia"}
print(sorted(monday | tuesday))
print(sorted(monday & tuesday))
print(sorted(monday - tuesday))
print(sorted(monday ^ tuesday))['ahmed', 'bilal', 'dia', 'rafi']
['ahmed']
['bilal', 'rafi']
['bilal', 'dia', 'rafi']Cuatro símbolos, cuatro preguntas:
|— en cualquiera de los dos (unión). Todos los que vinieron entre los dos días.&— en ambos (intersección). Quien vino los dos días.-— en el primero y no en el segundo (diferencia). Quien vino solo el lunes.^— en exactamente uno (diferencia simétrica). Quien vino un día pero no los dos.
Fíjate en que - respeta el orden: monday - tuesday y tuesday - monday dan respuestas distintas. Los otros tres se leen igual en ambos sentidos.
Escribir esos cuatro con bucles ocuparía varias líneas cada uno, y es fácil equivocarse en al menos uno.
Qué puede entrar
La misma regla que para las claves de un diccionario: lo que metas tiene que ser inmutable.
s = set()
s.add([1, 2])TypeError: unhashable type: 'list'Palabra por palabra el mismo mensaje, por la misma razón — un conjunto también usa hash por debajo. Cadenas, números y tuplas van bien; listas y diccionarios no.
Un ejemplo completo
sales.py:
# Which products sold on both days, and which only on one
monday = ["pen", "bag", "pen", "ink"]
tuesday = ["ink", "bottle", "pen", "ink"]
mon = set(monday)
tue = set(tuesday)
print("Monday sold :", sorted(mon), f"({len(monday)} sales, {len(mon)} products)")
print("Tuesday sold :", sorted(tue), f"({len(tuesday)} sales, {len(tue)} products)")
print()
print("Either day :", sorted(mon | tue))
print("Both days :", sorted(mon & tue))
print("Monday only :", sorted(mon - tue))
print("Exactly one day:", sorted(mon ^ tue))
print()
print("Was ink sold on Monday?", "ink" in mon)Monday sold : ['bag', 'ink', 'pen'] (4 sales, 3 products)
Tuesday sold : ['bottle', 'ink', 'pen'] (4 sales, 3 products)
Either day : ['bag', 'bottle', 'ink', 'pen']
Both days : ['ink', 'pen']
Monday only : ['bag']
Exactly one day: ['bag', 'bottle']
Was ink sold on Monday? TrueTres cosas que merece la pena notar.
Las listas originales se conservaron. len(monday) informa de cuatro ventas, len(mon) de tres productos. Una vez convertido a conjunto, el número de ventas se pierde — así que el conjunto no es un sustituto de los datos, es una herramienta para hacer un tipo de pregunta sobre ellos.
sorted() antes de cada impresión. Sin él el orden parecería arbitrario, y podría cambiar entre ejecuciones.
Dice "ink" in mon, no "ink" in monday. Los dos dan True, pero buscar en un conjunto es más rápido que buscar en una lista — en una lista Python compara un elemento cada vez, en un conjunto va directo al sitio por el hash. Con cuatro cosas no significa nada; con cuarenta mil sí.
Cuando falla
AttributeError: 'dict' object has no attribute 'add' Algo escribió s = {} queriendo un conjunto, pero eso es un diccionario vacío. Escribe s = set().
TypeError: 'set' object is not subscriptable Algo escribió s[0]. Un conjunto no tiene orden, así que no tiene índices. Si necesitas un elemento concreto, probablemente querías una lista o un diccionario; si no, sorted(s) te da una lista.
KeyError: 'gone' Se le pidió a remove que quitara algo que el conjunto no contiene. Si cualquiera de los dos estados es normal, usa discard.
TypeError: unhashable type: 'list' Un intento de meter una lista en un conjunto. Conviértela con tuple(...).
El orden salió revuelto después de list(set(...)) Es lo esperado; los conjuntos no tienen orden. Si lo necesitas, usa el patrón seen de arriba, o sorted().
Comparé dos conjuntos con == y dio True a pesar de un orden distinto Y con razón. Dos conjuntos son iguales cuando contienen las mismas cosas — el orden no juega ningún papel. [1, 2] == [2, 1] es False en listas; {1, 2} == {2, 1} es True en conjuntos.
Paso 4 de 6 — Predice
Comprueba lo que has entendido
Se hacen dos contenedores vacíos. ¿Qué informa type()?
not_a_set = {}
real_set = set()
print(type(not_a_set))
print(type(real_set))- A<class 'dict'> <class 'set'>
- B<class 'set'> <class 'set'>
- C<class 'set'> <class 'dict'>
- D<class 'dict'> <class 'dict'>
¿Qué dos preguntas responden & y -?
monday = {"rafi", "ahmed", "bilal"}
tuesday = {"ahmed", "dia"}
print(sorted(monday & tuesday))
print(sorted(monday - tuesday))- A['bilal', 'rafi'] ['ahmed']
- B['ahmed', 'bilal', 'dia', 'rafi'] ['ahmed']
- C['ahmed'] ['bilal', 'rafi']
- D['ahmed'] ['dia']
Se pide el primer elemento de un conjunto. ¿Qué pasa?
tags = {"new", "sale"}
print(tags[0])- Anew
- Bsale
- CUn `TypeError` — un conjunto no tiene orden, así que no tiene índices
- DUn `KeyError`
Para responder necesitas una cuenta
Inicia sesión para comprobar tus respuestas
Las preguntas están arriba, y resolverlas mentalmente es lo que de verdad importa. Inicia sesión para ver las respuestas, las explicaciones y las pistas de tres niveles.
Tu turno
Escribe un archivo llamado visitors.py con tres listas de visitantes, una por día, donde algunos nombres aparezcan en más de un día.
Luego imprime:
- Cuántas visitas hubo cada día, y cuántas personas distintas
- Cuántas personas distintas en total entre los tres días
- Las personas que vinieron los tres días
- Las personas que vinieron solo el primer día
- Las personas que vinieron en exactamente uno de los dos primeros días
Luego dos experimentos:
- Quita los duplicados de dos maneras —
list(set(...))y el patrónseende arriba — e imprime los dos resultados lado a lado. ¿Cuál es la diferencia? - Prueba a crear un conjunto vacío con
{}y a llamar a.add()sobre él. ¿Qué error vuelve, y a qué tipo de objeto nombra el mensaje?
El segundo es pequeño, y verlo una vez con tus propios ojos significa que ese error ya no te costará diez minutos nunca más.
Paso 6 de 6
Reto — el quiz del capítulo
Diez preguntas de fácil a difícil. Las últimas son difíciles a propósito.
Inicia sesión para hacer el quiz