Posted on July 27, 2018· Updated on July 3, 2022

Conjuntos de Python: Qué, Por Qué y Cómo - Hoja de Trucos de Python

#python #basics
Image for Conjuntos de Python: Qué, Por Qué y Cómo - Hoja de Trucos de Python

Python viene equipado con varios tipos de datos incorporados para ayudarnos a organizar nuestros datos. Estas estructuras incluyen lists, dictionaries, tuples y sets.

De la documentación de Python 3

Un set es una colección no ordenada sin elementos duplicados. Los usos básicos incluyen la prueba de pertenencia y la eliminación de entradas duplicadas. Los objetos set también admiten operaciones matemáticas como unión, intersección, diferencia y diferencia simétrica

En este artículo, vamos a revisar cada uno de los elementos enumerados en la definición anterior. Empecemos de inmediato y veamos cómo podemos crearlos.

Inicialización de un Set

Hay dos formas de crear un set: una es usar la función incorporada set() y pasar una list de elementos, y la otra es usar las llaves {}.

Inicialización de un set usando la función incorporada set()

>>> s1 = set([1, 2, 3])
>>> s1
{1, 2, 3}
>>> type(s1)
<class 'set'>

Inicialización de un set usando llaves {}

>>> s2 = {3, 4, 5}
>>> s2
{3, 4, 5}
>>> type(s2)
<class 'set'>
>>>

Sets Vacíos

Al crear un set, asegúrate de no usar llaves vacías {} o obtendrás un diccionario vacío en su lugar.

>>> s = {}
>>> type(s)
<class 'dict'>

Es un buen momento para mencionar que, por simplicidad, todos los ejemplos proporcionados en este artículo usarán enteros de un solo dígito, pero los sets pueden contener todos los tipos de datos hashable que Python admite. En otras palabras, enteros, cadenas y tuplas, pero no elementos mutables como listas o diccionarios:

>>> s = {1, 'coffee', [4, 'python']}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

Ahora que sabes cómo crear un set y qué tipo de elementos puede contener, continuemos y veamos por qué siempre deberíamos tenerlos en nuestro arsenal.

Por qué deberías usarlos

Podemos escribir código de más de una manera. Algunas se consideran bastante malas, y otras, claras, concisas y mantenibles. O “pythonic”.

De The Hitchhiker’s Guide to Python

Cuando un desarrollador veterano de Python (un Pythonista) llama a partes del código como no "Pythonicas", generalmente quieren decir que esas líneas de código no siguen las pautas comunes y no logran expresar su intención de la manera que se considera la mejor (léase: más legible).

Empecemos a explorar la forma en que los sets de Python pueden ayudarnos no solo con la legibilidad, sino también con el tiempo de ejecución de nuestro programa.

Colección no ordenada de elementos

Primero lo primero: no puedes acceder a un objeto set usando índices.

>>> s = {1, 2, 3}
>>> s[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'set' object does not support indexing

O modificarlos con slices:

>>> s[0:2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'set' object is not subscriptable

PERO, si lo que necesitamos es eliminar duplicados, o realizar operaciones matemáticas como combinar listas (uniones), podemos, y SIEMPRE debemos usar sets.

Tengo que mencionar que al iterar, las listas superan en rendimiento a los sets, así que prefírelas si eso es lo que necesitas. ¿Por qué? Bueno, este artículo no pretende explicar el funcionamiento interno de los sets, pero aquí hay un par de enlaces donde puedes leer al respecto:

Sin elementos duplicados

Mientras escribía esto, no puedo dejar de pensar en todas las veces que usé un bucle for y la declaración if para verificar y eliminar elementos duplicados en una lista. Mi cara se pone roja al recordar que, más de una vez, escribí algo como esto:

>>> my_list = [1, 2, 3, 2, 3, 4]
>>> no_duplicate_list = []
>>> for item in my_list:
...     if item not in no_duplicate_list:
...             no_duplicate_list.append(item)
...
>>> no_duplicate_list
[1, 2, 3, 4]

O usé una comprensión de lista:

>>> my_list = [1, 2, 3, 2, 3, 4]
>>> no_duplicate_list = []
>>> [no_duplicate_list.append(item) for item in my_list if item not in no_duplicate_list]
[None, None, None, None]
>>> no_duplicate_list
[1, 2, 3, 4]

Pero está bien, nada de eso importa ahora porque tenemos los sets:

>>> my_list = [1, 2, 3, 2, 3, 4]
>>> no_duplicate_list = list(set(my_list))
>>> no_duplicate_list
[1, 2, 3, 4]

Rendimiento de los Sets

Ahora usemos el módulo timeit y veamos el tiempo de ejecución de las listas y los sets al eliminar duplicados:

>>> from timeit import timeit
>>> def no_duplicates(list):
...     no_duplicate_list = []
...     [no_duplicate_list.append(item) for item in list if item not in no_duplicate_list]
...     return no_duplicate_list
...
>>> # primero, veamos cómo se comporta la lista:
>>> print(timeit('no_duplicates([1, 2, 3, 1, 7])', globals=globals(), number=1000))
0.0018683355819786227
>>> from timeit import timeit
>>> # y el set:
>>> print(timeit('list(set([1, 2, 3, 1, 2, 3, 4]))', number=1000))
0.0010220493243764395
>>> # más rápido y más limpio =)

No solo escribimos menos líneas de código con sets que con comprensiones de listas, sino que también obtenemos un código más legible y rendidor.

Recuerda que los sets no están ordenados

No hay garantía de que al convertirlos de nuevo a una lista, se preserve el orden de los elementos.

Del Zen de Python:

Lo bello es mejor que lo feo.
Lo explícito es mejor que lo implícito.
Lo simple es mejor que lo complejo.
Lo plano es mejor que lo anidado.

¿No son los sets precisamente Bellos, Explícitos, Simples y Planos?

Pruebas de pertenencia

Cada vez que usamos una declaración if para verificar si un elemento está, por ejemplo, en una lista, estamos realizando una prueba de pertenencia:

my_list = [1, 2, 3]
>>> if 2 in my_list:
...     print('Yes, this is a membership test!')
...
# Yes, this is a membership test!

Y los sets son más eficientes que las listas al realizarlas:

>>> from timeit import timeit
>>> def in_test(iterable):
...     for i in range(1000):
...             if i in iterable:
...                     pass
...
>>> timeit('in_test(iterable)', setup="from __main__ import in_test; iterable = list(range(1000))", number=1000)
# 12.459663048726043
>>> from timeit import timeit
>>> def in_test(iterable):
...     for i in range(1000):
...             if i in iterable:
...                     pass
...
>>> timeit('in_test(iterable)', setup="from __main__ import in_test; iterable = set(range(1000))", number=1000)
# 0.12354438152988223

Estas pruebas provienen de este hilo de Stack Overflow.

Así que si estás haciendo comparaciones como esta en listas masivas, te acelerará bastante si conviertes esa lista en un set.

Añadir Elementos

Dependiendo del número de elementos a añadir, tendremos que elegir entre los métodos add() y update().

add() Añadirá un solo elemento:

>>> s = {1, 2, 3}
>>> s.add(4)
>>> s
{1, 2, 3, 4}

Y update() varios:

>>> s = {1, 2, 3}
>>> s.update([2, 3, 4, 5, 6])
>>> s
{1, 2, 3, 4, 5, 6}

Recuerda, los sets eliminan duplicados.

Eliminar Elementos

Si quieres que se te notifique cuando tu código intente eliminar un elemento que no está en el set, usa remove(). De lo contrario, discard() proporciona una alternativa adecuada:

>>> s = {1, 2, 3}
>>> s.remove(3)
>>> s
{1, 2}
>>> s.remove(3)
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# KeyError: 3

discard() no generará ningún error:

>>> s = {1, 2, 3}
>>> s.discard(3)
>>> s
{1, 2}
>>> s.discard(3)
>>> # ¡no pasa nada!

También podemos usar pop() para descartar un elemento aleatoriamente:

>>> s = {1, 2, 3, 4, 5}
>>> s.pop()  # elimina un elemento arbitrario
1
>>> s
{2, 3, 4, 5}

O clear() para eliminar todos los valores de un set:

>>> s = {1, 2, 3, 4, 5}
>>> s.clear()  # descarta todos los elementos
>>> s
set()

El método union()

union() o | crearán un nuevo set que contiene todos los elementos de los sets que proporcionemos:

>>> s1 = {1, 2, 3}
>>> s2 = {3, 4, 5}
>>> s1.union(s2)  # o 's1 | s2'
{1, 2, 3, 4, 5}

El método intersection()

intersection o & devolverán un set que contiene solo los elementos que son comunes en todos ellos:

>>> s1 = {1, 2, 3}
>>> s2 = {2, 3, 4}
>>> s3 = {3, 4, 5}
>>> s1.intersection(s2, s3)  # o 's1 & s2 & s3'
{3}

El método difference()

Diferencia crea un nuevo set con los valores que están en “s1” pero no en “s2”:

>>> s1 = {1, 2, 3}
>>> s2 = {2, 3, 4}
>>> s1.difference(s2)  # o 's1 - s2'
{1}

symmetric_difference()

symmetric_difference o ^ devolverán todos los valores que no son comunes entre los sets.

>>> s1 = {1, 2, 3}
>>> s2 = {2, 3, 4}
>>> s1.symmetric_difference(s2)  # o 's1 ^ s2'
{1, 4}

Conclusiones

Espero que después de leer este artículo sepas qué es un set, cómo manipular sus elementos y las operaciones que pueden realizar. Saber cuándo usar un set definitivamente te ayudará a escribir código más limpio y a acelerar tus programas.