2.10. Conjuntos

Un conjunto es una colección desordenada de elementos únicos. Añadir un valor que ya está presente no tiene efecto; la iteración produce cada valor exactamente una vez. Los conjuntos son la herramienta adecuada cuando importan la pertenencia y la eliminación de duplicados, y el orden no.

2.10.1. Crear un conjunto

Usa llaves para un conjunto no vacío, o set() para uno vacío:

colours = {"red", "green", "blue"}
empty = set()

Las llaves se parecen a un literal de dict; {} por sí solo es un dict vacío, no un conjunto vacío – uno de los accidentes históricos de Python. Usa set() para el caso vacío.

set() también construye un conjunto a partir de cualquier iterable, que es la forma estándar de eliminar duplicados de una secuencia:

nums = [1, 2, 2, 3, 1, 4]
unique = set(nums)
print(unique)

Salida:

{1, 2, 3, 4}

El orden de impresión puede variar – los conjuntos no garantizan iterar en ningún orden concreto.

2.10.2. Conjunto frente a diccionario

Tanto los conjuntos como los diccionarios almacenan elementos únicos en una tabla hash. Lo que lleva consigo cada elemento es la diferencia:

  • Un dict almacena pares clave-valor. Buscar una clave devuelve su valor.

  • Un set almacena solo los elementos. Buscar un elemento te dice si está ahí.

La elección entre los dos depende de si el valor que acompaña a cada elemento significa algo:

  • Recurre a un conjunto cuando ningún valor corresponde junto a cada elemento – solo te importa si el elemento está presente, o estás combinando grupos de elementos únicos con unión / intersección.

  • Recurre a un diccionario cuando cada elemento está emparejado con datos que la búsqueda debe recuperar – un mapa de configuración, una caché, un contador indexado por nombre.

Los dos tipos comparten gran parte de la sintaxis superficial, que es de donde proviene la mayor parte de la confusión. Las diferencias en un bloque:

set

dict

contiene

elementos únicos

claves únicas, cada una con un valor

literal poblado

{1, 2, 3}

{"a": 1, "b": 2}

literal vacío

set()

{}

prueba de pertenencia

x in s

k in d (solo claves)

obtener un valor

n/d

d[k]

añadir un elemento

s.add(x)

d[k] = v

iterar

produce elementos

produce claves (usa d.items() para pares)

La asimetría entre los literales poblado y vacío es la trampa que merece la pena destacar:

  • Las llaves con elementos dentro{1, 2, 3} – son un literal de conjunto; las llaves con pares clave-valor{"a": 1} – son un literal de diccionario. El analizador los distingue por lo que hay dentro.

  • Las llaves con nada dentro{} – son un diccionario vacío, no un conjunto vacío. Los diccionarios vinieron primero; el literal vacío les pertenece. Un conjunto vacío no tiene ningún literal de llaves y debe escribirse set().

Un patrón común cuando solo se leen las claves de un diccionario es cambiar a un conjunto – hace evidente la intención y elimina de la memoria los valores no utilizados.

2.10.3. Añadir y eliminar

s = {1, 2, 3}
s.add(4)
s.discard(99)            # silent: 99 not in s
s.remove(2)
print(s)

Salida:

{1, 3, 4}

2.10.4. Pertenencia

El operador in comprueba la pertenencia. En un conjunto es aproximadamente de tiempo constante independientemente del tamaño – que es la razón principal para elegir un conjunto en lugar de una list cuando solo necesitas preguntar «¿está este valor ahí?»:

if "red" in colours:
    print("colour is allowed")

Una list con el mismo contenido recorrería desde el principio cada vez, lo cual está bien para diez elementos pero es lento para diez mil.

2.10.5. Operaciones de conjuntos

Dos conjuntos pueden combinarse con las operaciones matemáticas habituales. Cada una tiene tanto una forma de operador como una forma de método:

  • a | b o a.union(b) – todo lo que está en cualquiera de los dos conjuntos.

  • a & b o a.intersection(b) – solo lo que aparece en ambos.

  • a - b o a.difference(b) – lo que está en a pero no en b.

  • a ^ b o a.symmetric_difference(b) – lo que está en uno pero no en ambos.

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b)
print(a & b)
print(a - b)
print(a ^ b)

Salida:

{1, 2, 3, 4, 5, 6}
{3, 4}
{1, 2}
{1, 2, 5, 6}

Las formas de operador son de solo lectura; las formas de método aceptan cualquier iterable a la derecha, no solo otro conjunto (a.union([5, 6])). Elige la que se lea mejor en el contexto.

2.10.6. Qué puede ir en un conjunto

Los elementos de un conjunto deben ser hashables – la misma restricción que las claves de un dict. int, float, str, bool, bytes y tuple (cuando su contenido es a su vez hashable) funcionan todos. list y dict no; intentar añadir uno lanza TypeError.

2.10.7. frozenset

Un set normal es mutable: cada llamada a add / remove / discard cambia el objeto en el sitio. Esa mutabilidad lo descalifica para ser hashable, así que un conjunto no puede usarse como clave de un dict ni como miembro de otro conjunto.

frozenset es la contraparte inmutable. Tiene las mismas búsquedas y operadores (in, |, &, -, ^) que set, pero sin add / remove ni ningún método que mute. Como nada puede cambiar nunca su contenido, el hash de un frozenset está bien definido – así que es hashable:

primary = frozenset({"red", "green", "blue"})
secondary = frozenset({"yellow", "purple", "orange"})

palettes = {
    primary: "RGB",
    secondary: "mixed",
}

print(palettes[primary])

Salida:

RGB

Construye un frozenset a partir de cualquier iterable – frozenset() para el caso vacío, frozenset(some_set) para tomar una captura inmutable de un conjunto existente:

snapshot = frozenset(s)         # immutable copy of s
s.add("new")                    # snapshot does not change

Dos razones comunes para recurrir a él:

  • Uso como clave de diccionario o miembro de conjunto. Dondequiera que un único valor no pueda capturar lo que necesitas, un frozenset de valores sí puede – «el conjunto de características que admite este controlador», «el conjunto de pines que usa este perfil».

  • Blindar una constante. Un frozenset a nivel de módulo de nombres permitidos no puede ser mutado accidentalmente por un llamador; un set normal sí. Prefiere frozenset para cualquier cosa que deba ser de solo lectura tras su construcción.