17. Set: lavorare con dati unici e non ordinati
Nei capitoli precedenti, abbiamo lavorato con liste (collezioni ordinate e mutabili) e dizionari (mappature chiave-valore). Ora esploreremo i set, il tipo di collezione di Python progettato specificamente per memorizzare elementi unici ed eseguire operazioni matematiche tra insiemi in modo efficiente.
I set sono particolarmente potenti quando devi eliminare i duplicati, testare rapidamente l’appartenenza, o eseguire operazioni come trovare elementi comuni tra collezioni. A differenza delle liste, i set non sono ordinati e non possono contenere valori duplicati: tentare di aggiungere lo stesso elemento due volte non ha alcun effetto.
17.1) Creare set e operazioni di base
17.1.1) Creare set con parentesi graffe
Il modo più comune per creare un set è usare le parentesi graffe {} con valori separati da virgole:
# Creazione di un set di linguaggi di programmazione
languages = {"Python", "JavaScript", "Java", "C++"}
print(languages) # Output: {'Python', 'JavaScript', 'Java', 'C++'}
print(type(languages)) # Output: <class 'set'>Importante: l’ordine degli elementi quando stampi un set può differire dall’ordine in cui li hai inseriti. I set sono collezioni non ordinate, il che significa che Python non mantiene alcuna sequenza particolare:
numbers = {5, 2, 8, 1, 9}
print(numbers) # L'output potrebbe essere: {1, 2, 5, 8, 9} o un altro ordineL’ordine dell’output può variare tra esecuzioni e versioni di Python. Non fare mai affidamento sul fatto che i set mantengano un ordine specifico—se l’ordine conta, usa invece una lista.
17.1.2) I set rimuovono automaticamente i duplicati
Una delle proprietà più utili dei set è che eliminano automaticamente i valori duplicati. Se provi a creare un set con elementi duplicati, viene mantenuta solo una copia di ciascun valore univoco:
# Creazione di un set con valori duplicati
student_ids = {101, 102, 103, 102, 101, 104}
print(student_ids) # Output: {101, 102, 103, 104}
# Questa proprietà rende i set perfetti per rimuovere duplicati
grades = [85, 90, 85, 78, 90, 92, 78, 85]
unique_grades = set(grades)
print(unique_grades) # Output: {78, 85, 90, 92}Questa deduplicazione automatica avviene perché i set usano un modello matematico di insieme in cui ogni elemento può apparire una sola volta. Quando aggiungi un valore che esiste già, il set semplicemente ignora il duplicato.
17.1.3) Creare set con il costruttore set()
Puoi creare set da altri iterabili(iterable) usando il costruttore set(). Questo è particolarmente utile per convertire liste, tuple o stringhe in set:
# Creazione di un set da una lista
colors_list = ["red", "blue", "green", "red", "yellow"]
colors_set = set(colors_list)
print(colors_set) # Output: {'red', 'blue', 'green', 'yellow'}
# Creazione di un set da una stringa (ogni carattere diventa un elemento)
letters = set("programming")
print(letters) # Output: {'p', 'r', 'o', 'g', 'a', 'm', 'i', 'n'}
# Creazione di un set da una tupla
coordinates = set((10, 20, 30, 20, 10))
print(coordinates) # Output: {10, 20, 30}Quando crei un set da una stringa, ogni carattere univoco diventa un elemento separato. Questo è utile per trovare tutti i caratteri distinti in un testo:
text = "Mississippi"
unique_chars = set(text.lower())
print(unique_chars) # Output: {'m', 'i', 's', 'p'}
print(f"The word contains {len(unique_chars)} unique letters")
# Output: The word contains 4 unique letters17.1.4) Creare un set vuoto
Ecco un tranello critico: non puoi creare un set vuoto usando {} perché Python lo interpreta come un dizionario vuoto. Devi invece usare set():
# ERRATO - Questo crea un dizionario vuoto, non un set
empty_dict = {}
print(type(empty_dict)) # Output: <class 'dict'>
# CORRETTO - Questo crea un set vuoto
empty_set = set()
print(type(empty_set)) # Output: <class 'set'>
print(empty_set) # Output: set()Questa distinzione esiste perché i dizionari sono stati aggiunti a Python prima dei set, quindi {} era già stato assegnato ai dizionari vuoti. Quando stampi un set vuoto, Python lo mostra come set() per evitare confusione.
Confusione comune per i principianti: quando crei un set con un singolo elemento usando una variabile, il set contiene il valore della variabile, non il nome della variabile:
# Comprendere la creazione di set con variabili
x = 5
my_set = {x} # Crea {5}, non {'x'}
print(my_set) # Output: {5}
# Se vuoi un set che contenga la stringa 'x':
my_set = {'x'}
print(my_set) # Output: {'x'}
# Questo vale per qualsiasi espressione
result = 10 + 5
my_set = {result} # Crea {15}
print(my_set) # Output: {15}17.1.5) Proprietà e operazioni di base dei set
I set supportano diverse operazioni fondamentali che li rendono utili per l’elaborazione dei dati:
# Verificare il numero di elementi unici
website_visitors = {"alice", "bob", "charlie", "alice", "david"}
print(f"Unique visitors: {len(website_visitors)}")
# Output: Unique visitors: 4
# Verificare l'appartenenza con 'in' (molto veloce per i set)
if "alice" in website_visitors:
print("Alice visited the website")
# Output: Alice visited the website
# Verificare la non appartenenza
if "eve" not in website_visitors:
print("Eve has not visited yet")
# Output: Eve has not visited yetIl test di appartenenza con in è uno dei vantaggi chiave dei set. Per collezioni grandi, verificare se un elemento esiste in un set è molto più veloce che verificare in una lista. Esploreremo perché questo conta nella Sezione 17.5.
17.2) Aggiungere e rimuovere elementi dai set
A differenza delle tuple (che sono immutabili), i set sono mutabili—puoi aggiungere e rimuovere elementi dopo la creazione. Tuttavia, gli elementi stessi devono essere tipi immutabili (esploreremo questa restrizione nella Sezione 17.7).
17.2.1) Aggiungere singoli elementi con add()
Aggiungere elementi individuali a un set è semplice con il metodo add(). Se l’elemento esiste già, il set rimane invariato—non viene generato alcun errore e non viene creato alcun duplicato:
# Costruire un set di attività completate
completed_tasks = {"task1", "task2"}
print(completed_tasks) # Output: {'task1', 'task2'}
# Aggiungere una nuova attività
completed_tasks.add("task3")
print(completed_tasks) # Output: {'task1', 'task2', 'task3'}
# Aggiungere un duplicato non ha alcun effetto
completed_tasks.add("task1")
print(completed_tasks) # Output: {'task1', 'task2', 'task3'}Questo comportamento rende i set ideali per tracciare occorrenze uniche. Puoi chiamare add() in modo sicuro senza controllare se l’elemento esiste già—il set gestisce i duplicati automaticamente.
17.2.2) Aggiungere più elementi con update()
Per aggiungere più elementi in una sola volta, usa update() che accetta qualsiasi iterabile (lista, tupla, un altro set, ecc.) e aggiunge tutti i suoi elementi al set:
# Partire con un piccolo set di competenze
skills = {"Python", "SQL"}
print(skills) # Output: {'Python', 'SQL'}
# Aggiungere più competenze da una lista
new_skills = ["JavaScript", "Docker", "Python"]
skills.update(new_skills)
print(skills) # Output: {'Python', 'SQL', 'JavaScript', 'Docker'}Nota che "Python" compare sia nel set originale sia nella lista aggiunta, ma il set contiene comunque una sola copia. Il metodo update() può accettare più iterabili come argomenti:
# Combinare competenze da più fonti
current_skills = {"Python"}
course_skills = ["JavaScript", "HTML"]
job_requirements = {"SQL", "Python", "Docker"}
current_skills.update(course_skills, job_requirements)
print(current_skills)
# Output: {'Python', 'JavaScript', 'HTML', 'SQL', 'Docker'}17.2.3) Rimuovere elementi con remove()
Rimuovere elementi richiede attenzione. Il metodo remove() elimina un elemento da un set, ma solleva un KeyError se l’elemento non esiste:
# Gestire utenti attivi
active_users = {"alice", "bob", "charlie", "david"}
# Rimuovere un utente che ha effettuato il logout
active_users.remove("bob")
print(active_users) # Output: {'alice', 'charlie', 'david'}
# Tentare di rimuovere un elemento inesistente causa un errore
# active_users.remove("eve") # Raises: KeyError: 'eve'Poiché remove() solleva un errore per gli elementi mancanti, è meglio usarlo quando sei certo che l’elemento esista, o quando vuoi intercettare l’errore se non esiste:
# Rimozione sicura con gestione degli errori (impareremo di più su try/except nel Capitolo 28)
users = {"alice", "bob", "charlie"}
user_to_remove = "david"
if user_to_remove in users:
users.remove(user_to_remove)
print(f"Removed {user_to_remove}")
else:
print(f"{user_to_remove} was not in the set")
# Output: david was not in the set17.2.4) Rimuovere elementi in modo sicuro con discard()
Per una rimozione più sicura degli elementi che non generi errori, discard() fornisce un’alternativa più permissiva. Rimuove l’elemento se presente, ma non fa nulla se l’elemento non esiste:
# Gestire un carrello della spesa
cart_items = {"apple", "banana", "orange"}
# Rimuovere elementi in modo sicuro (nessun errore se l'elemento non esiste)
cart_items.discard("banana")
print(cart_items) # Output: {'apple', 'orange'}
cart_items.discard("grape") # Nessun errore, anche se grape non è nel set
print(cart_items) # Output: {'apple', 'orange'}Usa discard() quando vuoi assicurarti che un elemento non sia nel set, indipendentemente dal fatto che ci fosse all’inizio. Usa remove() quando l’assenza dell’elemento indica una condizione di errore che vuoi intercettare.
17.2.5) Rimuovere e restituire un elemento arbitrario con pop()
Il metodo pop() rimuove e restituisce un elemento arbitrario dal set. Poiché i set non sono ordinati, non puoi prevedere quale elemento verrà rimosso:
# Elaborare una coda di attività in sospeso (l'ordine non conta)
pending_tasks = {"email", "report", "meeting", "review"}
# Elabora un'attività (non importa quale)
task = pending_tasks.pop()
print(f"Processing: {task}") # Output: Processing: email (or another task)
print(f"Remaining: {pending_tasks}")
# Output: Remaining: {'report', 'meeting', 'review'} (without the popped task)Se chiami pop() su un set vuoto, solleva un KeyError:
empty_set = set()
# empty_set.pop() # Raises: KeyError: 'pop from an empty set'Il metodo pop() è utile quando devi elaborare tutti gli elementi in un set ma non ti interessa l’ordine:
# Elaborare tutti gli elementi in un set
items_to_process = {"item1", "item2", "item3"}
while items_to_process:
item = items_to_process.pop()
print(f"Processing {item}")
# Elaborare l'elemento...
print("All items processed")
# Output:
# Processing item1
# Processing item2
# Processing item3
# All items processed17.2.6) Rimuovere tutti gli elementi con clear()
Il metodo clear() rimuove tutti gli elementi da un set, lasciandolo vuoto:
# Reimpostare i dati di una sessione
session_data = {"user_id", "timestamp", "ip_address"}
print(session_data) # Output: {'user_id', 'timestamp', 'ip_address'}
session_data.clear()
print(session_data) # Output: set()
print(len(session_data)) # Output: 0Questo è più efficiente che creare un nuovo set vuoto se vuoi riutilizzare lo stesso oggetto set.
17.3) Operazioni tra set: unione, intersezione, differenza e differenza simmetrica
I set supportano operazioni matematiche tra insiemi che ti permettono di combinare, confrontare e analizzare collezioni in modo efficiente. Queste operazioni sono fondamentali nella teoria degli insiemi e hanno molte applicazioni pratiche nell’elaborazione dei dati.
17.3.1) Unione: combinare set
Iniziamo con uno scenario pratico per capire perché l’unione è importante. Immagina di gestire le iscrizioni degli studenti tra corsi diversi:
# Studenti iscritti a corsi diversi
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
# Trovare tutti gli studenti che seguono uno dei due corsi (o entrambi)
all_students = python_students | javascript_students
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david', 'eve'}L’unione di due set contiene tutti gli elementi che compaiono in uno dei due set (o in entrambi). Python fornisce due modi per calcolare le unioni: l’operatore | (mostrato sopra) e il metodo union():
# Stesso risultato usando il metodo union()
all_students = python_students.union(javascript_students)
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david', 'eve'}Il metodo union() può accettare più set come argomenti, rendendo comodo combinare dati da molte fonti:
# Studenti in tre corsi diversi
python_students = {"alice", "bob"}
javascript_students = {"bob", "charlie"}
sql_students = {"charlie", "david"}
# Tutti gli studenti in tutti i corsi
all_students = python_students.union(javascript_students, sql_students)
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david'}Un altro esempio di unione è combinare liste email di reparti diversi:
# Combinare liste email di reparti diversi
marketing_contacts = {"alice@company.com", "bob@company.com"}
sales_contacts = {"bob@company.com", "charlie@company.com"}
support_contacts = {"david@company.com", "alice@company.com"}
# Tutti i contatti unici tra i reparti
all_contacts = marketing_contacts | sales_contacts | support_contacts
print(f"Total unique contacts: {len(all_contacts)}")
# Output: Total unique contacts: 417.3.2) Intersezione: trovare elementi comuni
Capire quali elementi compaiono in più set è cruciale per molte attività di analisi dei dati. L’operazione di intersezione risponde alla domanda: "Cosa hanno in comune questi set?"
# Trovare i clienti che hanno acquistato entrambi i prodotti
customers_product_a = {101, 102, 103, 104, 105}
customers_product_b = {103, 104, 105, 106, 107}
# Clienti che hanno acquistato entrambi i prodotti
both_products = customers_product_a & customers_product_b
print(f"Bought both: {both_products}")
# Output: Bought both: {103, 104, 105}L’intersezione contiene solo gli elementi che compaiono in entrambi i set. Puoi anche usare il metodo intersection(), che accetta più set:
# Trovare gli studenti iscritti a tutti e tre i corsi
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "charlie", "david"}
sql_students = {"charlie", "eve", "bob"}
# Studenti che seguono tutti e tre i corsi
all_three = python_students.intersection(javascript_students, sql_students)
print(all_three) # Output: {'bob', 'charlie'}Ecco un caso d’uso pratico per trovare prodotti disponibili in più magazzini:
# Trovare prodotti disponibili in più magazzini
warehouse_a = {"laptop", "mouse", "keyboard", "monitor"}
warehouse_b = {"mouse", "keyboard", "printer", "scanner"}
warehouse_c = {"keyboard", "monitor", "mouse", "desk"}
# Prodotti disponibili in tutti i magazzini
available_everywhere = warehouse_a & warehouse_b & warehouse_c
print(f"Available in all locations: {available_everywhere}")
# Output: Available in all locations: {'mouse', 'keyboard'}17.3.3) Differenza: trovare elementi in un set ma non in un altro
A volte devi identificare ciò che è unico in una collezione. L’operazione di differenza trova gli elementi che sono nel primo set ma non nel secondo:
# Gestione inventario: trovare discrepanze
expected_items = {"item001", "item002", "item003", "item004"}
actual_items = {"item001", "item003", "item005"}
# Elementi mancanti nell'inventario
missing = expected_items - actual_items
print(f"Missing items: {missing}")
# Output: Missing items: {'item002', 'item004'}
# Elementi inattesi nell'inventario
unexpected = actual_items - expected_items
print(f"Unexpected items: {unexpected}")
# Output: Unexpected items: {'item005'}Puoi anche usare il metodo difference():
# Studenti solo nel corso Python (non in JavaScript)
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
python_only = python_students.difference(javascript_students)
print(python_only) # Output: {'alice', 'charlie'}Importante: l’operazione di differenza non è commutativa—l’ordine conta:
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
# Studenti in Python ma non in JavaScript
python_only = python_students - javascript_students
print(f"Python only: {python_only}")
# Output: Python only: {'alice', 'charlie'}
# Studenti in JavaScript ma non in Python
javascript_only = javascript_students - python_students
print(f"JavaScript only: {javascript_only}")
# Output: JavaScript only: {'david', 'eve'}17.3.4) Differenza simmetrica: elementi in uno dei due set ma non in entrambi
La differenza simmetrica trova gli elementi che sono in uno dei due set ma non in entrambi. Questa operazione è particolarmente utile per identificare cambiamenti tra due versioni:
# Confrontare due versioni di una configurazione
old_settings = {"debug", "logging", "cache", "compression"}
new_settings = {"logging", "cache", "monitoring", "security"}
# Impostazioni cambiate (aggiunte o rimosse)
changes = old_settings ^ new_settings
print(f"Changed settings: {changes}")
# Output: Changed settings: {'debug', 'compression', 'monitoring', 'security'}
# Per vedere nello specifico cosa è stato aggiunto vs rimosso:
removed = old_settings - new_settings
added = new_settings - old_settings
print(f"Removed: {removed}") # Output: Removed: {'debug', 'compression'}
print(f"Added: {added}") # Output: Added: {'monitoring', 'security'}Puoi anche usare il metodo symmetric_difference():
# Studenti in esattamente un corso (non in entrambi)
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
one_course_only = python_students.symmetric_difference(javascript_students)
print(one_course_only)
# Output: {'alice', 'charlie', 'david', 'eve'}A differenza della differenza, la differenza simmetrica è commutativa—l’ordine non conta:
result1 = python_students ^ javascript_students
result2 = javascript_students ^ python_students
print(result1 == result2) # Output: True17.4) Relazioni di sottoinsieme e sovrainsieme (issubset, issuperset, isdisjoint)
Oltre a combinare i set, spesso dobbiamo capire le relazioni tra di essi. Python fornisce metodi per verificare se un set è contenuto in un altro, se ne contiene un altro, o se non condivide alcun elemento con un altro.
17.4.1) Testare i sottoinsiemi con issubset() e <=
Un set A è un sottoinsieme di un set B se ogni elemento di A è anche in B. In altre parole, B contiene tutti gli elementi di A (e possibilmente altri).
# Prerequisiti del corso
basic_skills = {"reading", "writing"}
intermediate_skills = {"reading", "writing", "analysis"}
# Verificare se le competenze di base sono un sottoinsieme delle competenze intermedie
print(basic_skills.issubset(intermediate_skills)) # Output: True
print(basic_skills <= intermediate_skills) # Output: True (same result)Un set è sempre un sottoinsieme di sé stesso:
skills = {"Python", "SQL", "JavaScript"}
print(skills.issubset(skills)) # Output: True
print(skills <= skills) # Output: TrueSe vuoi verificare un sottoinsieme proprio (A è un sottoinsieme di B ma non uguale a B), usa l’operatore <:
basic_skills = {"reading", "writing"}
intermediate_skills = {"reading", "writing", "analysis"}
# Sottoinsieme proprio: basic è un sottoinsieme di intermediate E non sono uguali
print(basic_skills < intermediate_skills) # Output: True
# Non è un sottoinsieme proprio di sé stesso (sono uguali)
print(basic_skills < basic_skills) # Output: FalseUn esempio pratico del test di sottoinsieme è controllare permessi o requisiti:
# Sistema di permessi utente
required_permissions = {"read", "write"}
user_permissions = {"read", "write", "delete", "admin"}
# Verificare se l'utente ha tutti i permessi richiesti
if required_permissions.issubset(user_permissions):
print("Access granted")
else:
print("Access denied - missing permissions")
# Output: Access granted
# Un altro utente con permessi insufficienti
limited_user = {"read"}
if required_permissions.issubset(limited_user):
print("Access granted")
else:
missing = required_permissions - limited_user
print(f"Access denied - missing: {missing}")
# Output: Access denied - missing: {'write'}17.4.2) Testare i sovrainsiemi con issuperset() e >=
Un set A è un sovrainsieme di un set B se A contiene tutti gli elementi di B. Questa è la relazione inversa del sottoinsieme—se A è un sottoinsieme di B, allora B è un sovrainsieme di A.
# Livelli di competenze
basic_skills = {"reading", "writing"}
advanced_skills = {"reading", "writing", "analysis", "research"}
# Verificare se advanced_skills è un sovrainsieme di basic_skills
print(advanced_skills.issuperset(basic_skills)) # Output: True
print(advanced_skills >= basic_skills) # Output: True (same result)Come per i sottoinsiemi, un set è sempre un sovrainsieme di sé stesso:
skills = {"Python", "SQL"}
print(skills.issuperset(skills)) # Output: TruePer un sovrainsieme proprio (A è un sovrainsieme di B ma non uguale a B), usa l’operatore >:
basic_skills = {"reading", "writing"}
advanced_skills = {"reading", "writing", "analysis"}
# Sovrainsieme proprio: advanced contiene tutto basic E ha di più
print(advanced_skills > basic_skills) # Output: True
# Non è un sovrainsieme proprio di sé stesso
print(advanced_skills > advanced_skills) # Output: False17.4.3) Testare set disgiunti con isdisjoint()
Due set sono disgiunti se non hanno elementi in comune—la loro intersezione è vuota. Il metodo isdisjoint() restituisce True se i set non condividono alcun elemento:
# Verificare conflitti nella pianificazione
morning_classes = {"math", "english", "history"}
afternoon_classes = {"science", "art", "music"}
# Verificare se ci sono conflitti (stessa lezione in entrambe le sessioni)
if morning_classes.isdisjoint(afternoon_classes):
print("No scheduling conflicts")
else:
conflicts = morning_classes & afternoon_classes
print(f"Conflicts: {conflicts}")
# Output: No scheduling conflictsQuando i set non sono disgiunti:
morning_classes = {"math", "english", "history"}
afternoon_classes = {"science", "math", "music"}
if morning_classes.isdisjoint(afternoon_classes):
print("No scheduling conflicts")
else:
conflicts = morning_classes & afternoon_classes
print(f"Conflicts: {conflicts}")
# Output: Conflicts: {'math'}I set vuoti sono disgiunti con tutti i set (inclusi altri set vuoti):
empty = set()
numbers = {1, 2, 3}
print(empty.isdisjoint(numbers)) # Output: True
print(empty.isdisjoint(empty)) # Output: True17.5) Quando usare i set invece delle liste
Capire quando usare i set rispetto alle liste è cruciale per scrivere codice Python efficiente. Anche se entrambi memorizzano collezioni di elementi, hanno caratteristiche diverse che rendono ciascuno adatto a compiti differenti.
17.5.1) Usa i set per test di appartenenza rapidi
Uno dei vantaggi più significativi dei set è la velocità nel test di appartenenza. Verificare se un elemento esiste in un set è molto più veloce che verificare in una lista, specialmente per collezioni grandi:
# Verificare se un utente è in una grande collezione
active_users_list = []
for i in range(10000):
active_users_list.append("user" + str(i))
# Con una lista (lento per collezioni grandi)
print("user5000" in active_users_list) # Checks each element until found
active_users_set = set()
for i in range(10000):
active_users_set.add("user" + str(i))
# Con un set (veloce indipendentemente dalla dimensione)
print("user5000" in active_users_set) # Direct lookupAnche se entrambi producono lo stesso risultato, la versione con set è drasticamente più veloce per collezioni grandi. Questo perché i set usano internamente una tabella hash, permettendo ricerche quasi istantanee indipendentemente dalla dimensione, mentre le liste devono controllare ogni elemento in sequenza.
17.5.2) Usa i set per eliminare duplicati
Quando devi rimuovere duplicati da una collezione, convertire in un set è l’approccio più semplice:
# Rimuovere voci duplicate dall'input utente
survey_responses = [
"yes", "no", "yes", "maybe", "yes", "no", "maybe", "yes"
]
# Ottenere risposte uniche
unique_responses = set(survey_responses)
print(unique_responses) # Output: {'yes', 'no', 'maybe'}
# Se ti serve di nuovo una lista (con duplicati rimossi)
unique_list = list(unique_responses)
print(unique_list) # Output: ['yes', 'no', 'maybe'] (order may vary)17.5.3) Usa i set per operazioni matematiche tra insiemi
Quando devi trovare elementi comuni, differenze o unioni tra collezioni, i set forniscono operazioni chiare ed efficienti:
# Analizzare i pattern di acquisto dei clienti
customers_product_a = {101, 102, 103, 104, 105}
customers_product_b = {103, 104, 105, 106, 107}
# Clienti che hanno acquistato entrambi i prodotti
both_products = customers_product_a & customers_product_b
print(f"Bought both: {both_products}")
# Output: Bought both: {103, 104, 105}
# Clienti che hanno acquistato solo il prodotto A
only_a = customers_product_a - customers_product_b
print(f"Only product A: {only_a}")
# Output: Only product A: {101, 102}
# Tutti i clienti che hanno acquistato almeno un prodotto
all_customers = customers_product_a | customers_product_b
print(f"Total customers: {len(all_customers)}")
# Output: Total customers: 717.5.4) Usa le liste quando l’ordine conta
I set non sono ordinati, quindi se la sequenza degli elementi è importante, devi usare una lista:
# ERRATO - L'ordine non è preservato con i set
task_order = {"wake up", "breakfast", "work", "lunch", "work", "dinner"}
print(task_order) # L'ordine è imprevedibile e "work" compare una sola volta
# CORRETTO - Usa una lista quando l'ordine conta
task_order = ["wake up", "breakfast", "work", "lunch", "work", "dinner"]
print(task_order)
# Output: ['wake up', 'breakfast', 'work', 'lunch', 'work', 'dinner']17.5.5) Usa le liste quando i duplicati sono significativi
Se i valori duplicati portano informazione (come la frequenza o occorrenze multiple), usa una lista:
# Registrare punteggi di un quiz (i duplicati mostrano quanti studenti hanno ottenuto ciascun punteggio)
quiz_scores = [85, 90, 85, 78, 90, 92, 85, 88]
# Con una lista, possiamo contare le occorrenze
score_85_count = quiz_scores.count(85)
print(f"Students who scored 85: {score_85_count}")
# Output: Students who scored 85: 3
# Con un set, perderemmo questa informazione
unique_scores = set(quiz_scores)
print(unique_scores) # Output: {78, 85, 88, 90, 92}
# Non possiamo sapere quanti studenti hanno ottenuto ciascun punteggio17.5.6) Usa le liste quando ti serve l’indicizzazione
I set non supportano l’indicizzazione perché non sono ordinati. Se devi accedere agli elementi per posizione, usa una lista:
# ERRATO - I set non supportano l'indicizzazione
colors = {"red", "blue", "green"}
# first_color = colors[0] # Raises: TypeError: 'set' object is not subscriptable
# CORRETTO - Usa una lista per l'accesso tramite indice
colors = ["red", "blue", "green"]
first_color = colors[0]
print(first_color) # Output: red17.6) Frozenset e set immutabili
Finora abbiamo lavorato con set normali, che sono mutabili—puoi aggiungere e rimuovere elementi dopo la creazione. Python fornisce anche i frozenset, che sono versioni immutabili dei set. Una volta creato, un frozenset non può essere modificato.
17.6.1) Creare frozenset
Crei un frozenset usando il costruttore frozenset(), in modo simile a come crei un set normale con set():
# Creare un frozenset da una lista
colors = frozenset(["red", "blue", "green"])
print(colors) # Output: frozenset({'red', 'blue', 'green'})
print(type(colors)) # Output: <class 'frozenset'>
# Creare un frozenset da una tupla
numbers = frozenset((1, 2, 3, 4, 5))
print(numbers) # Output: frozenset({1, 2, 3, 4, 5})
# Creare un frozenset vuoto
empty = frozenset()
print(empty) # Output: frozenset()Come i set normali, i frozenset eliminano automaticamente i duplicati:
# I duplicati vengono rimossi
values = frozenset([1, 2, 2, 3, 3, 3, 4])
print(values) # Output: frozenset({1, 2, 3, 4})17.6.2) I frozenset sono immutabili
Una volta creato, non puoi modificare un frozenset. Metodi come add(), remove(), discard(), pop() e clear() non esistono per i frozenset:
# Creare un frozenset
languages = frozenset(["Python", "JavaScript", "Java"])
# Tentare di modificare genera un errore
# languages.add("C++") # AttributeError: 'frozenset' object has no attribute 'add'
# languages.remove("Java") # AttributeError: 'frozenset' object has no attribute 'remove'Questa immutabilità è la caratteristica distintiva dei frozenset. Se hai bisogno di "modificare" un frozenset, devi crearne uno nuovo:
# Frozenset originale
original = frozenset([1, 2, 3])
# Creare un nuovo frozenset con un elemento aggiuntivo
modified = frozenset(list(original) + [4])
print(original) # Output: frozenset({1, 2, 3})
print(modified) # Output: frozenset({1, 2, 3, 4})17.6.3) Le operazioni tra set funzionano con i frozenset
I frozenset supportano tutte le stesse operazioni tra insiemi dei set normali (unione, intersezione, differenza, ecc.):
# Operazioni tra set con frozenset
set_a = frozenset([1, 2, 3, 4])
set_b = frozenset([3, 4, 5, 6])
# Unione
print(set_a | set_b) # Output: frozenset({1, 2, 3, 4, 5, 6})
# Intersezione
print(set_a & set_b) # Output: frozenset({3, 4})
# Differenza
print(set_a - set_b) # Output: frozenset({1, 2})
# Differenza simmetrica
print(set_a ^ set_b) # Output: frozenset({1, 2, 5, 6})Puoi anche mescolare set normali e frozenset nelle operazioni:
regular_set = {1, 2, 3}
frozen_set = frozenset([3, 4, 5])
# Operazioni tra set normali e frozenset
result = regular_set | frozen_set
print(result) # Output: {1, 2, 3, 4, 5}
print(type(result)) # Output: <class 'set'> (result is a regular set)17.6.4) Perché usare i frozenset?
Il motivo principale per usare i frozenset è che possono essere usati come chiavi di dizionari o come elementi in altri set, cosa che i set normali non possono fare:
# ERRATO - I set normali non possono essere chiavi di dizionario
# regular_set = {1, 2, 3}
# my_dict = {regular_set: "value"} # TypeError: unhashable type: 'set'
# CORRETTO - I frozenset possono essere chiavi di dizionario
frozen_set = frozenset([1, 2, 3])
my_dict = {frozen_set: "value"}
print(my_dict) # Output: {frozenset({1, 2, 3}): 'value'}
print(my_dict[frozen_set]) # Output: valueUn esempio pratico usando frozenset come chiavi di dizionario:
# Memorizzare informazioni su coppie di coordinate
# Ogni coordinata è un frozenset di valori (x, y)
location_data = {
frozenset([0, 0]): "origin",
frozenset([1, 0]): "east",
frozenset([1, 1]): "northeast"
}
# Cercare una posizione
point = frozenset([1, 0])
print(location_data[point]) # Output: eastI frozenset possono anche essere elementi in altri set:
# ERRATO - I set normali non possono essere elementi di un set
# set_of_sets = {{1, 2}, {3, 4}} # TypeError: unhashable type: 'set'
# CORRETTO - I frozenset possono essere elementi di un set
set_of_frozensets = {
frozenset([1, 2]),
frozenset([3, 4]),
frozenset([5, 6])
}
print(set_of_frozensets)
# Output: {frozenset({1, 2}), frozenset({3, 4}), frozenset({5, 6})}Un esempio pratico che rappresenta gruppi:
# Rappresentare squadre in cui ogni squadra è un frozenset di ID giocatori
tournament_teams = {
frozenset([101, 102, 103]), # Team A
frozenset([201, 202, 203]), # Team B
frozenset([301, 302, 303]) # Team C
}
# Verificare se una squadra specifica è registrata
team_to_check = frozenset([101, 102, 103])
if team_to_check in tournament_teams:
print("Team is registered")
else:
print("Team not found")
# Output: Team is registered17.6.5) Convertire tra set e frozenset
Puoi convertire facilmente tra set normali e frozenset:
# Convertire un set normale in un frozenset
regular = {1, 2, 3, 4}
frozen = frozenset(regular)
print(frozen) # Output: frozenset({1, 2, 3, 4})
# Convertire un frozenset in un set normale
frozen = frozenset([5, 6, 7, 8])
regular = set(frozen)
print(regular) # Output: {5, 6, 7, 8}
# Ora possiamo modificare il set normale
regular.add(9)
print(regular) # Output: {5, 6, 7, 8, 9}17.7) Tipi hashable e unhashable: cosa può essere chiave di dizionario o elemento di un set (e una breve nota sull’hashing)
In questo capitolo, abbiamo visto che i set possono contenere alcuni tipi di oggetti ma non altri. Per esempio, puoi creare un set di interi o stringhe, ma non un set di liste. Questa restrizione esiste perché gli elementi dei set (e le chiavi dei dizionari, come abbiamo imparato nel Capitolo 16) devono essere hashable.
17.7.1) Cosa significa "hashable"?
Un oggetto hashable è un oggetto che ha un valore hash che non cambia mai durante la sua vita. Python calcola questo valore hash usando una funzione integrata chiamata hash():
# I tipi hashable hanno un valore hash
print(hash(42)) # Output: 42
print(hash("Python")) # Output: (some large integer)
print(hash((1, 2, 3))) # Output: (some large integer)Il valore hash è un intero che Python usa internamente per localizzare rapidamente oggetti in set e dizionari. Pensalo come un indirizzo o un indice che aiuta Python a trovare le cose in modo efficiente.
Proprietà chiave: perché un oggetto sia hashable, il suo valore hash deve rimanere costante per tutta la sua vita. Questo significa che l’oggetto stesso deve essere immutabile—se l’oggetto potesse cambiare, anche il suo valore hash dovrebbe cambiare, cosa che romperebbe set e dizionari.
17.7.2) I tipi immutabili sono hashable
Tutti i tipi built-in immutabili di Python sono hashable e possono essere usati come elementi di set o chiavi di dizionario:
# Gli interi sono hashable
numbers = {1, 2, 3, 4, 5}
print(numbers) # Output: {1, 2, 3, 4, 5}
# Le stringhe sono hashable
words = {"apple", "banana", "cherry"}
print(words) # Output: {'apple', 'banana', 'cherry'}
# Le tuple sono hashable (se contengono solo elementi hashable)
coordinates = {(0, 0), (1, 1), (2, 2)}
print(coordinates) # Output: {(0, 0), (1, 1), (2, 2)}
# I frozenset sono hashable
frozen_sets = {frozenset([1, 2]), frozenset([3, 4])}
print(frozen_sets) # Output: {frozenset({1, 2}), frozenset({3, 4})}
# Booleani e None sono hashable
mixed = {True, False, None, 42, "text"}
print(mixed) # Output: {False, True, None, 42, 'text'}17.7.3) I tipi mutabili non sono hashable
I tipi mutabili come liste, set normali e dizionari non sono hashable perché il loro contenuto può cambiare:
# Le liste NON sono hashable
# my_set = {[1, 2, 3]} # TypeError: unhashable type: 'list'
# I set normali NON sono hashable
# set_of_sets = {{1, 2}, {3, 4}} # TypeError: unhashable type: 'set'
# I dizionari NON sono hashable
# my_set = {{"key": "value"}} # TypeError: unhashable type: 'dict'Perché la mutabilità conta? Considera cosa succederebbe se potessimo aggiungere una lista a un set:
# Scenario ipotetico (in realtà questo non funziona)
# my_list = [1, 2, 3]
# my_set = {my_list} # Supponiamo che questo funzionasse
#
# # Python calcola l'hash in base a [1, 2, 3]
# # Ora modifichiamo la lista:
# my_list.append(4) # Ora è [1, 2, 3, 4]
#
# # Il valore hash sarebbe errato! Il set verrebbe corrotto.Ecco perché Python impedisce che oggetti mutabili siano nei set o usati come chiavi di dizionario—romperebbe la struttura dati interna.
Confusione comune per i principianti: anche se i set stessi sono mutabili (puoi aggiungere e rimuovere elementi), gli elementi devono essere immutabili. I principianti a volte provano a modificare oggetti dopo averli aggiunti ai set, senza rendersi conto di questa distinzione concettuale:
# Confusione comune: il set è mutabile, ma gli elementi devono essere immutabili
# Il set è mutabile - puoi modificarne i contenuti
fruits = {'apple', 'banana'}
fruits.add('orange') # ✓ Works
fruits.remove('apple') # ✓ Works
# Ma gli elementi devono essere immutabili - non possono essere cambiati
my_list = [1, 2, 3]
# my_set = {my_list} # ✗ TypeError: unhashable type: 'list'
# Perché? Se potessi modificare my_list dopo averlo aggiunto, la struttura interna del set
# sarebbe corrotta.
# Questo funziona perché le tuple sono immutabili
my_tuple = (1, 2, 3)
my_set = {my_tuple} # ✓ Works - tuples can't be modified17.7.4) Il caso speciale delle tuple
Le tuple sono hashable solo se tutti i loro elementi sono hashable. Una tupla che contiene oggetti mutabili non è hashable:
# Tupla con soli elementi immutabili - hashable
good_tuple = (1, 2, "three")
my_set = {good_tuple} # Funziona: good_tuple è hashable
print(my_set) # Output: {(1, 2, 'three')}
# Tupla che contiene una lista - NON hashable
bad_tuple = (1, 2, [3, 4])
# my_set = {bad_tuple} # TypeError: unhashable type: 'list'Questo ha senso: anche se la tupla in sé è immutabile (non puoi cambiare quali oggetti contiene), se uno di quegli oggetti è mutabile, il "valore" complessivo della tupla può cambiare:
# Dimostrazione del perché le tuple con elementi mutabili non possono essere hashate
inner_list = [1, 2]
my_tuple = (inner_list, 3)
# La struttura della tupla è fissa, ma la lista all'interno può cambiare
inner_list.append(3) # Ora inner_list è [1, 2, 3]
# La tupla ora "contiene" dati diversi, ma è lo stesso oggetto tupla17.7.5) Verificare la hashability
Puoi verificare se un oggetto è hashable provando a calcolarne l’hash:
# Verificare la hashability
def is_hashable(obj):
"""Verifica se un oggetto è hashable."""
try:
hash(obj)
return True
except TypeError:
return False
# Testare vari tipi
print(is_hashable(42)) # Output: True
print(is_hashable("text")) # Output: True
print(is_hashable((1, 2, 3))) # Output: True
print(is_hashable([1, 2, 3])) # Output: False
print(is_hashable({1, 2, 3})) # Output: False
print(is_hashable({"key": "value"})) # Output: False17.7.6) Riepilogo dei tipi hashable
Hashable (possono essere elementi di set o chiavi di dizionario):
- Interi:
42 - Float:
3.14 - Stringhe:
"text" - Tuple (se tutti gli elementi sono hashable):
(1, 2, "three") - Frozenset:
frozenset([1, 2, 3]) - Booleani:
True,False - None:
None
Non hashable (non possono essere elementi di set o chiavi di dizionario):
- Liste:
[1, 2, 3] - Set normali:
{1, 2, 3} - Dizionari:
{"key": "value"} - Tuple contenenti elementi non hashable:
(1, [2, 3])
Comprendere la hashability ti aiuta a scegliere le strutture dati giuste ed evitare errori comuni quando lavori con set e dizionari. Il principio chiave è semplice: se un oggetto può cambiare, non può essere hashato; se non può essere hashato, non può stare in un set o essere usato come chiave di un dizionario.