2.10. Insiemi

Un insieme (set) è una collezione non ordinata di elementi unici. Aggiungere un valore già presente non ha alcun effetto; l’iterazione produce ogni valore esattamente una volta. Gli insiemi sono lo strumento giusto quando contano l’appartenenza e la deduplicazione, e l’ordinamento no.

2.10.1. Creare un insieme

Usa le parentesi graffe per un insieme non vuoto, oppure set() per uno vuoto:

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

Le parentesi graffe sembrano un letterale dict; {} da solo è un dict vuoto, non un insieme vuoto – uno degli incidenti storici di Python. Usa set() per il caso vuoto.

set() costruisce inoltre un insieme a partire da qualsiasi iterabile, che è il modo standard per eliminare i duplicati da una sequenza:

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

Output:

{1, 2, 3, 4}

L’ordine di stampa può variare – gli insiemi non garantiscono di iterare in alcun ordine particolare.

2.10.2. Set vs dict

Sia gli insiemi che i dict memorizzano elementi unici in una tabella hash. La differenza sta in ciò che ogni elemento porta con sé:

  • Un dict memorizza coppie chiave-valore. La ricerca di una chiave restituisce il suo valore.

  • Un set memorizza solo gli elementi. La ricerca di un elemento dice se è presente.

La scelta tra i due dipende dal fatto che il valore accanto a ciascun elemento significhi qualcosa:

  • Scegli un set quando nessun valore va affiancato a ciascun elemento – ti interessa solo se l’elemento è presente, oppure stai combinando gruppi di elementi unici con unione / intersezione.

  • Scegli un dict quando ciascun elemento è abbinato a dati che la ricerca deve recuperare – una mappa di configurazione, una cache, un contatore indicizzato per nome.

I due tipi condividono molta sintassi di superficie, ed è da qui che proviene la maggior parte della confusione. Le differenze in un unico blocco:

set

dict

contiene

elementi unici

chiavi uniche, ciascuna con un valore

letterale popolato

{1, 2, 3}

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

letterale vuoto

set()

{}

test di appartenenza

x in s

k in d (solo chiavi)

recuperare un valore

n/d

d[k]

aggiungere un elemento

s.add(x)

d[k] = v

iterare

produce elementi

produce chiavi (usa d.items() per le coppie)

L’asimmetria tra i letterali popolati e quelli vuoti è il tranello che vale la pena evidenziare:

  • Le parentesi graffe con elementi al loro interno{1, 2, 3} – sono un letterale set; le parentesi graffe con coppie chiave-valore{"a": 1} – sono un letterale dict. Il parser le distingue in base a ciò che c’è all’interno.

  • Le parentesi graffe senza nulla all’interno{} – sono un dict vuoto, non un insieme vuoto. I dict sono arrivati prima; il letterale vuoto appartiene a loro. Un insieme vuoto non ha alcun letterale con parentesi graffe e deve essere scritto set().

Uno schema comune, quando di un dict vengono lette solo le chiavi, è passare a un set – rende ovvio l’intento e libera dalla memoria i valori inutilizzati.

2.10.3. Aggiungere e rimuovere

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

Output:

{1, 3, 4}

2.10.4. Appartenenza

L’operatore in verifica l’appartenenza. Su un insieme il suo tempo è all’incirca costante indipendentemente dalla dimensione – che è il motivo principale per scegliere un set rispetto a una list quando ti serve solo chiedere «questo valore è lì dentro»:

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

Una list con lo stesso contenuto eseguirebbe ogni volta una scansione dall’inizio, il che va bene per dieci elementi ma è lento per diecimila.

2.10.5. Operazioni sugli insiemi

Due insiemi possono essere combinati con le consuete operazioni matematiche. Ciascuna ha sia una forma con operatore sia una forma con metodo:

  • a | b o a.union(b) – tutto ciò che è in uno o nell’altro insieme.

  • a & b o a.intersection(b) – solo ciò che appare in entrambi.

  • a - b o a.difference(b) – ciò che è in a ma non in b.

  • a ^ b o a.symmetric_difference(b) – ciò che è in uno ma non in entrambi.

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

Output:

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

Le forme con operatore sono di sola lettura; le forme con metodo accettano a destra qualsiasi iterabile, non solo un altro insieme (a.union([5, 6])). Scegli quella che si legge meglio nel contesto.

2.10.6. Cosa può finire in un insieme

Gli elementi di un insieme devono essere hashable – lo stesso vincolo delle chiavi di un dict. int, float, str, bool, bytes e tuple (quando il suo contenuto è a sua volta hashable) funzionano tutti. list e dict no; tentare di aggiungerne uno solleva TypeError.

2.10.7. frozenset

Un set normale è mutabile: ogni chiamata a add / remove / discard modifica l’oggetto in posizione. Questa mutabilità lo squalifica dall’essere hashable, quindi un set non può essere usato come chiave di un dict né come membro di un altro insieme.

frozenset è la controparte immutabile. Ha le stesse ricerche e operatori (in, |, &, -, ^) di set, ma nessun add / remove e nessun metodo che muta. Poiché nulla può mai cambiare il suo contenuto, l’hash di un frozenset è ben definito – quindi è hashable:

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

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

print(palettes[primary])

Output:

RGB

Costruisci un frozenset a partire da qualsiasi iterabile – frozenset() per il caso vuoto, frozenset(some_set) per prendere uno snapshot immutabile di un insieme esistente:

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

Due motivi comuni per ricorrervi:

  • Usalo come chiave di un dict o membro di un set. Ovunque un singolo valore non riesca a catturare ciò che ti serve, un frozenset di valori può farlo – «l’insieme di caratteristiche supportate da questo driver», «l’insieme di pin usati da questo profilo».

  • Blocca una costante. Un frozenset a livello di modulo di nomi consentiti non può essere mutato accidentalmente da un chiamante; un set normale sì. Preferisci frozenset per tutto ciò che è destinato a essere di sola lettura dopo la costruzione.