Conjuntos (sets)

Conjuntos (sets)



Un conjunto es una colección no ordenada de objetos únicos. Python provee este tipo de datos “por defecto” al igual que otras colecciones más convencionales como las listas, tuplas y diccionarios.

Los conjuntos son ampliamente utilizados en lógica y matemática, y desde el lenguaje podemos sacar provecho de sus propiedades para crear código más eficiente y legible en menos tiempo.

Creación de un conjunto

Para crear un conjunto especificamos sus elementos entre llaves:

s = {1, 2, 3, 4}

Al igual que otras colecciones, sus miembros pueden ser de diversos tipos:

>>> s = {True, 3.14, None, False, "Hola mundo", (1, 2)}

No obstante, un conjunto no puede incluir objetos mutables como listas, diccionarios, e incluso otros conjuntos.

>>> s = {[1, 2]}
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'list'

Python distingue este tipo operación de la creación de un diccionario ya que no incluye dos puntos. Sin embargo, no puede dirimir el siguiente caso:

s = {}

Por defecto, la asignación anterior crea un diccionario. Para generar un conjunto vacío, directamente creamos una instancia de la clase set:

s = set()

De la misma forma podemos obtener un conjunto a partir de cualquier objeto iterable:

s1 = set([1, 2, 3, 4])
s2 = set(range(10))

Un set puede ser convertido a una lista y viceversa. En este último caso, los elementos duplicados son unificados.

>>> list({1, 2, 3, 4})
[1, 2, 3, 4]
>>> set([1, 2, 2, 3, 4])
{1, 2, 3, 4}

Elementos

Los conjuntos son objetos mutables. Vía los métodos add() y discard() podemos añadir y remover un elemento indicándolo como argumento.

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

Para determinar si un elemento pertenece a un conjunto, utilizamos la palabra reservada in.

>>> 2 in {1, 2, 3}
True
>>> 4 in {1, 2, 3}
False

La función clear() elimina todos los elementos.

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

El método pop() retorna un elemento en forma aleatoria (no podría ser de otra manera ya que los elementos no están ordenados). Así, el siguiente bucle imprime y remueve uno por uno los miembros de un conjunto.

while s:
    print(s.pop())

remove() y pop() lanzan la excepción KeyError cuando un elemento no se encuentra en el conjunto o bien éste está vacío, respectivamente.

Para obtener el número de elementos aplicamos la ya conocida función len():

>>> len({1, 2, 3, 4})
4

Operaciones principales

Algunas de las propiedades más interesantes de los conjuntos radican en sus operaciones principales: unión, intersección y diferencia.

La unión se realiza con el caracter | y retorna un conjunto que contiene los elementos que se encuentran en al menos uno de los dos conjuntos involucrados en la operación.

>>> a = {1, 2, 3, 4}
>>> b = {3, 4, 5, 6}
>>> a | b
{1, 2, 3, 4, 5, 6}

La intersección opera de forma análoga, pero con el operador &, y retorna un nuevo conjunto con los elementos que se encuentran en ambos.

>>> a & b
{3, 4}

La diferencia, por último, retorna un nuevo conjunto que contiene los elementos de a que no están en b.

>>> a = {1, 2, 3, 4}
>>> b = {2, 3}
>>> a - b
{1, 4}

Dos conjuntos son iguales si y solo si contienen los mismos elementos (a esto se lo conoce como principio de extensionalidad):

>>> {1, 2, 3} == {3, 2, 1}
True
>>> {1, 2, 3} == {4, 5, 6}
False

Otras operaciones

Se dice que B es un subconjunto de A cuando todos los elementos de aquél están pertenecen también a éste. Python puede determinar esta relación vía el método issubset().

>>> a = {1, 2, 3, 4}
>>> b = {2, 3}
>>> b.issubset(a)
True

Inversamente, se dice que A es un superconjunto de B.

>>> a.issupperset(b)
True

La definición de estas dos relaciones nos lleva a concluir que todo conjunto es al mismo tiempo un subconjunto y un superconjunto de sí mismo.

>>> a = {1, 2, 3, 4}
>>> a.issubset(a)
True
>>> a.issuperset(a)
True

La diferencia simétrica retorna un nuevo conjunto el cual contiene los elementos que pertenecen a alguno de los dos conjuntos que participan en la operación pero no a ambos. Podría entenderse como una unión exclusiva.

>>> a = {1, 2, 3, 4}
>>> b = {3, 4, 5, 6}
>>> a.symmetric_difference(b)
{1, 2, 5, 6}

Dada esta definición, se infiere que es indistinto el orden de los objetos:

>>> b.symmetric_difference(a)
{1, 2, 5, 6}

Por último, se dice que un conjunto es disconexo respecto de otro si no comparten elementos entre sí.

>>> a = {1, 2, 3}
>>> b = {3, 4, 5}
>>> c = {5, 6, 7}
>>> a.isdisjoint(b)
False  # No son disconexos ya que comparten el elemento 3.
>>> a.isdisjoint(c)
True   # Son disconexos.

En otras palabras, dos conjuntos son disconexos si su intersección es el conjunto vacío, por lo que puede ilustrarse de la siguiente forma:

>>> def isdisjoint(a, b):
...     return a & b == set()
...
>>> isdisjoint(a, b)
False
>>> isdisjoint(a, c)
True

Conjuntos inmutables

frozenset es una implementación similar a set pero inmutable. Es decir, comparte todas las operaciones de conjuntos provistas en este artículo a excepción de aquellas que implican alterar sus elementos (add(), discard(), etc.). La diferencia es análoga a la existente entre una lista y una tupla.

>>> a = frozenset({1, 2, 3})
>>> b = frozenset({3, 4, 5})
>>> a & b
frozenset({3})
>>> a | b
frozenset({1, 2, 3, 4, 5})
>>> a.isdisjoint(b)
False

Esto permite, por ejemplo, emplear conjuntos como claves en los diccionarios:

>>> a = {1, 2, 3}
>>> b = frozenset(a)
>>> {a: 1}
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'set'
>>> {b: 1}
{frozenset({1, 2, 3}): 1}

Ejemplos

¿Qué aplicaciones reales le conciernen a los conjuntos? Consideremos un programa que solicite al usuario ingresar una indeterminada cantidad de números e indique cuáles de ellos son primos.

# Solicitar entrada del usuario.
numbers = input("Ingrese números separados por espacios: ")
# Convertir a una lista de números enteros.
numbers = [int(n) for n in numbers.split(" ")]

Ahora bien, haciendo uso de la función get_prime_numbers() diseñada en un artículo anterior para obtener números primos, la solución convencional (empleando listas) se vería más o menos así:

prime_numbers = [n for n in numbers
                 if n in get_prime_numbers(max(numbers))]

No obstante, si trabajamos con conjuntos, la solución es aún más corta y eficiente:

prime_numbers = numbers & get_prime_numbers(max(numbers))

Considerando la siguiente modificación:

numbers = {int(n) for n in numbers.split(" ")}

Y en la última línea de la función:

# Retorna un conjunto.
return {i + 2 for i, not_crossed in enumerate(numbers[2:]) if not_crossed}



Deja un comentario