Liste, dizionari, set, cicli e comprehension
Quando usare ciascuno dei quattro contenitori di Python, cicli e condizioni, comprehension, e contare le parole in ticket disordinati per trovare una classifica delle prime cinque parole che esce sempre uguale.
Come vuoi imparare?
Il problema che stiamo risolvendo
Il responsabile del supporto clienti vuole sapere di cosa si sono lamentati di più i clienti nei ticket di questa settimana. I ticket sono testo disordinato — alcuni in maiuscolo, alcuni pieni di punteggiatura, alcuni che dicono due volte la stessa cosa. Un collega ha scritto in cinque minuti uno script e ha mandato una lista: the in cima, poi refund, poi Refund e refund. come voci separate.
La seconda esecuzione ha portato un altro problema: un piccolo cambio nell'ordine dei ticket ha cambiato la quinta parola delle prime cinque, anche se il conteggio di nessuna parola era cambiato. Nessuno che leggeva il report poteva capire quale versione fosse quella giusta.
La risposta non è una libreria speciale. Sono i quattro contenitori base di Python — liste, tuple, dizionari e set — e cicli e condizioni usati nel modo giusto. Gran parte del lavoro quotidiano di un ingegnere AI è esattamente questo: pulire il testo, contare, filtrare e ordinare, in un modo che dia sempre lo stesso risultato per lo stesso input.
Alla fine di questo capitolo sarai in grado di
- Scegliere tra lista, tupla, dizionario e set per un compito, e spiegare perché
- Scrivere cicli puliti con
for,while,if/elif/else,enumeratee.items() - Filtrare e trasformare dati con comprehension di liste, dizionari e set
- Estrarre parole da un testo disordinato e contarle con un dizionario e con
Counter - Ordinare le parole con lo stesso conteggio secondo una regola esplicita, così il risultato è sempre uguale
Prerequisiti: pathlib, configurazione JSON/YAML e variabili d'ambiente — e saper eseguire uno script con uv run.
Quattro contenitori, quattro compiti
Prima, i quattro uno accanto all'altro:
tickets = ["refund", "login", "refund", "billing"]
latest = ("T-1042", "refund", 3)
owners = {"refund": "Maya", "login": "Omar"}
seen = set(tickets)
print(tickets[0], tickets[-1], len(tickets))
print(latest[1])
print(owners["login"])
print(sorted(seen))
print("login" in seen, "shipping" in seen)refund billing 4
refund
Omar
['billing', 'login', 'refund']
True FalseOgnuno fa una promessa diversa, e scegli in base alla promessa:
- Lista (
[...]) — mantiene l'ordine, ammette lo stesso valore più di una volta, e può essere modificata. Per «una fila di cose che arrivano una dopo l'altra»: una lista di ticket, un batch di prompt. - Tupla (
(...)) — mantiene l'ordine ma non può essere modificata dopo la creazione. Per pochi valori che insieme formano un unico record, come("T-1042", "refund", 3)— id, categoria, priorità. Qui la posizione ha un significato. - Dizionario (
{chiave: valore}) — cerca un valore tramite una chiave. «Chi è il proprietario di quale categoria», «quante volte compare ogni parola» — ovunque si passa da una cosa a un'altra. - Set (
set(...)o{a, b}) — ogni valore compare una sola volta, senza un ordine garantito. Per quando la domanda è solo «è presente?» o «quanti valori distinti ci sono».
sorted(seen) viene stampato perché l'ordine di un set stampato non è garantito — può apparire in un modo in una esecuzione e in un altro in quella successiva. Se l'output di un programma dipende dall'ordine di un set, anche quell'output non sarà stabile.
insu una lista controlla un elemento alla volta; su un set o un dizionario risponde quasi subito, che ci siano dieci elementi o dieci milioni. Se controlliif word in stop_wordstante volte,stop_wordsdovrebbe essere un set, non una lista. Il perché e come misurare la differenza arriveranno più avanti nel corso.
Cicli: di cosa stai scorrendo gli elementi
I cicli su tuple e dizionari si leggono meglio con l'unpacking — i valori finiscono direttamente in variabili con un nome:
ticket_id, category, priority = ("T-1042", "refund", 3)
print(ticket_id, category, priority)
owners = {"refund": "Maya", "login": "Omar"}
for category, owner in owners.items():
print(f"{category} -> {owner}")
for position, category in enumerate(["refund", "login"], start=1):
print(position, category)T-1042 refund 3
refund -> Maya
login -> Omar
1 refund
2 login.items() restituisce una coppia (chiave, valore) a ogni giro, e enumerate restituisce ogni elemento insieme alla sua posizione — niente indici da contare con range(len(...)). A partire da Python 3.7 un dizionario ricorda l'ordine in cui sono state inserite le chiavi, quindi il ciclo su owners parte da refund.
Cosa succede se dimentichi .items()?
owners = {"refund": "Maya", "login": "Omar"}
for category, owner in owners:
print(category, owner)Traceback (most recent call last):
File "main.py", line 2, in <module>
for category, owner in owners:
^^^^^^^^^^^^^^^
ValueError: too many values to unpack (expected 2)Scorrere direttamente un dizionario dà solo le chiavi. La prima chiave, "refund", è una stringa di sei caratteri, e Python prova a spezzarla in due variabili — sei caratteri, due posti. Il messaggio sembra strano finché non ricordi che anche una stringa è una sequenza.
if/elif/else si ferma alla prima condizione vera, quindi l'ordine delle condizioni è la logica:
latencies_ms = [120, 340, 95, 1800, 210, 2600]
for latency in latencies_ms:
if latency >= 2000:
label = "critical"
elif latency >= 1000:
label = "slow"
else:
label = "ok"
print(latency, label)120 ok
340 ok
95 ok
1800 slow
210 ok
2600 criticalScrivi le condizioni nell'ordine opposto — prima>= 1000, poi>= 2000— e non c'è nessun errore, ma2600diventaslow, perché la prima condizione è vera e la catena si ferma lì. Il ramocriticalnon viene mai eseguito. Quando dividi per soglie, metti per primo la condizione più stringente.
for scorre una collezione nota. while continua finché una condizione è vera — quando non sai in anticipo quante volte servirà. Scaricare dati da un'API pagina per pagina è il caso classico. Qui un dizionario fa la parte dell'API:
pages = {1: ["t1", "t2"], 2: ["t3"], 3: []}
page = 1
collected = []
while True:
batch = pages[page]
if not batch:
break
collected.extend(batch)
page += 1
print(collected, "pages read:", page - 1)['t1', 't2', 't3'] pages read: 2Una pagina vuota significa break. if not batch funziona perché una lista vuota è falsa. In cicli come questo, scrivi prima la condizione di uscita e il resto dopo — un while True che dimentica di fermarsi gira per sempre, senza nessun errore.
Comprehension: una collezione a partire da un'altra
«Una lista vuota, un ciclo, un if, un append» — questo schema di quattro righe è così comune che Python ha una forma in una riga:
latencies_ms = [120, 340, 95, 1800, 210, 2600]
slow = [ms for ms in latencies_ms if ms >= 1000]
seconds = [ms / 1000 for ms in latencies_ms]
labels = {ms: ("slow" if ms >= 1000 else "ok") for ms in latencies_ms}
buckets = {ms // 1000 for ms in latencies_ms}
print(slow)
print(seconds)
print(labels)
print(sorted(buckets))[1800, 2600]
[0.12, 0.34, 0.095, 1.8, 0.21, 2.6]
{120: 'ok', 340: 'ok', 95: 'ok', 1800: 'slow', 210: 'ok', 2600: 'slow'}
[0, 1, 2]Come leggerlo: [risultato for elemento in collezione if condizione]. La parte sinistra dice cosa costruire da ogni elemento; l'if finale dice quali tenere. La stessa forma con {chiave: valore for ...} costruisce un dizionario, e {valore for ...} un set — buckets contiene tre bucket distinti da un secondo, ciascuno una volta sola.
Non confondere i due tipi di if. L'if ms >= 1000 finale è un filtro — quando è falso, l'elemento viene scartato. "slow" if ms >= 1000 else "ok" è una scelta — ogni elemento resta, e il suo valore è uno dei due. Per questo labels ha tutti e sei.
Una comprehension serve a costruire una nuova collezione. Quando vuoi solo stampare qualcosa o scrivere su un file — quando il risultato verrebbe buttato via — scrivi un normale for. E una comprehension annidata su tre livelli può stare su una riga, ma un ciclo di cinque righe è spesso più facile da leggere per un collega.
Contare le parole: a mano, poi con Counter
Quasi tutti, al primo tentativo di contare con un dizionario, scrivono così:
words = ["refund", "late", "refund", "login", "refund", "late"]
counts = {}
for word in words:
counts[word] += 1Traceback (most recent call last):
File "main.py", line 5, in <module>
counts[word] += 1
~~~~~~^^^^^^
KeyError: 'refund'counts[word] += 1 significa counts[word] = counts[word] + 1 — e leggere counts[word] a destra scopre che la chiave "refund" non esiste ancora. Una parola vista per la prima volta ha bisogno di un valore iniziale. .get(chiave, 0) lo fornisce esattamente:
words = ["refund", "late", "refund", "login", "refund", "late"]
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts){'refund': 3, 'late': 2, 'login': 1}Il compito è così comune che la libreria standard ha un tipo apposta — collections.Counter, una specie di dizionario:
from collections import Counter
words = ["refund", "late", "login", "refund", "login", "late", "slow"]
counts = Counter(words)
print(counts)
print(counts.most_common(2))
print(counts["missing"])Counter({'refund': 2, 'late': 2, 'login': 2, 'slow': 1})
[('refund', 2), ('late', 2)]
0Leggere una chiave mancante da un Counter dà 0, non un KeyError — esattamente quello che serve per contare. Ma guarda most_common(2): refund, late e login hanno tutti un conteggio di 2, e ne sono usciti i primi due. Perché login è rimasto fuori? Perché, a parità di conteggio, most_common mantiene l'ordine in cui le parole sono state viste per la prima volta. login è apparso per primo in terza posizione nella lista, quindi è rimasto indietro. Cambia l'ordine dei ticket e anche il risultato cambia — il mistero dell'inizio del capitolo.
Pulire il testo: quali parole sono davvero parole?
split() spezza solo sugli spazi:
text = "Refund not received. REFUND please! Login fails; login page is slow."
print(text.split())
print(text.lower().split())['Refund', 'not', 'received.', 'REFUND', 'please!', 'Login', 'fails;', 'login', 'page', 'is', 'slow.']
['refund', 'not', 'received.', 'refund', 'please!', 'login', 'fails;', 'login', 'page', 'is', 'slow.'].lower() ha risolto la differenza tra maiuscole e minuscole, ma received., please! e slow. portano ancora punteggiatura — e quando conti, slow. e slow sono due parole diverse. Qui un'espressione regolare aiuta: invece di dire cosa buttare via, dici come è fatta una parola:
import re
text = "Refund not received. REFUND please! Login fails; login page is slow. Can't pay."
words = re.findall(r"[a-z']+", text.lower())
print(words)['refund', 'not', 'received', 'refund', 'please', 'login', 'fails', 'login', 'page', 'is', 'slow', "can't", 'pay'][a-z']+ significa «una o più lettere inglesi minuscole o apostrofi». re.findall restituisce ogni corrispondenza del pattern nel testo, e tutto il resto — punti, virgole, cifre — viene scartato. can't resta una parola sola.
Questo pattern ha un buco: se qualcuno scrive'invalid token', la prima parola esce come'invalid— con l'apice attaccato. L'esempio successivo rende il pattern più stretto, così un apostrofo può stare solo tra due lettere. E ricorda che[a-z]riguarda solo le lettere inglesi; un testo in altri alfabeti richiede un pattern diverso.
Stesso conteggio: decidi tu la regola di ordinamento
Per trasformare i conteggi in una classifica devi ordinare. Tre modi, su un dizionario in cui refund e login hanno entrambi un conteggio di 3:
counts = {"refund": 3, "late": 2, "login": 3, "slow": 1}
print(sorted(counts, reverse=True))
print(sorted(counts.items(), key=lambda item: item[1], reverse=True))
print(sorted(counts.items(), key=lambda item: (-item[1], item[0])))['slow', 'refund', 'login', 'late']
[('refund', 3), ('login', 3), ('late', 2), ('slow', 1)]
[('login', 3), ('refund', 3), ('late', 2), ('slow', 1)]La prima riga è la più pericolosa, perché sembra ragionevole. Ordinare un dizionario ordina le sue chiavi, non i suoi valori — quindi è l'ordine alfabetico inverso e non ha nulla a che vedere con i conteggi. slow è comparso una sola volta, eppure è in cima. Nessun errore.
La seconda riga ordina per conteggio. Ma quale viene prima, refund o login? sorted di Python è «stabile» — gli elementi con chiavi uguali mantengono l'ordine che avevano in ingresso. refund è stato inserito nel dizionario per primo, quindi viene prima. In altre parole, il risultato dipende dall'ordine in cui sono arrivati i dati.
La terza riga dichiara la regola stessa. key restituisce una tupla, e le tuple si confrontano elemento per elemento: prima -item[1] — il conteggio negato, quindi i conteggi più alti vengono prima — e in caso di parità item[0], la parola stessa, in ordine alfabetico. Ora il risultato è lo stesso qualunque sia l'ordine dei dati in ingresso. Non serve reverse=True, perché il segno meno ha già invertito l'ordine dei conteggi lasciando l'ordine alfabetico com'è.
Ovunque i risultati verranno confrontati — report, test, valutazioni — dai una regola esplicita per ordinare i valori uguali. Altrimenti il report di oggi e quello di domani possono differire anche quando i dati non cambiano.
Un esempio completo
Ora mettiamo insieme tutto: le prime cinque parole dei ticket. Le stop words — parole come the e and che compaiono in ogni testo ma non dicono nulla — vengono scartate, così come le parole di due lettere, e i pareggi vengono risolti in ordine alfabetico. top_words.py:
import re
from collections import Counter
STOP_WORDS = {
"a", "and", "for", "i", "is", "it", "my", "not", "of", "on", "the", "to", "was", "with",
}
TICKETS = [
"Refund not received for order 1182. I was charged twice!",
"The app is slow and the login page freezes.",
"Charged twice again. Please refund the second charge.",
"Login fails with 'invalid token' on my phone.",
"Still waiting on my refund, it was promised last week.",
"App slow after the update; login takes a minute.",
]
def tokenize(text: str) -> list[str]:
return re.findall(r"[a-z]+(?:'[a-z]+)?", text.lower())
def top_words(texts: list[str], n: int = 5) -> list[tuple[str, int]]:
counts = Counter(
word
for text in texts
for word in tokenize(text)
if word not in STOP_WORDS and len(word) > 2
)
ranked = sorted(counts.items(), key=lambda item: (-item[1], item[0]))
return ranked[:n]
def print_ranking(ranking: list[tuple[str, int]]) -> None:
if not ranking:
print("no words to rank")
return
width = max(len(word) for word, _ in ranking)
for rank, (word, count) in enumerate(ranking, start=1):
print(f"{rank}. {word:<{width}} {count} {'#' * count}")
if __name__ == "__main__":
print_ranking(top_words(TICKETS))
print_ranking(top_words([]))uv run top_words.py1. login 3 ###
2. refund 3 ###
3. app 2 ##
4. charged 2 ##
5. slow 2 ##
no words to rankLe scelte che stanno dietro:
Il pattern delle parole [a-z]+(?:'[a-z]+)?. Una o più lettere, poi opzionalmente un apostrofo e altre lettere. can't resta una parola sola, ma l'apice attorno a 'invalid token' non fa più parte di una parola. (?:...) è un gruppo non catturato — con un semplice (...), re.findall restituirebbe solo la parte del gruppo invece dell'intera parola.
Le stop words sono un set. Ogni parola viene controllata con word not in STOP_WORDS; su un set quel controllo è quasi istantaneo, per quanto la lista cresca.
Counter alimentato direttamente da un generatore. Due for e un if formano un unico flusso — ogni parola di ogni ticket che supera il filtro. Non viene costruita nessuna grande lista intermedia con tutte le parole. I due for sono nello stesso ordine dei normali cicli annidati: prima quello esterno.
Uno spareggio esplicito. login e refund hanno entrambi 3; app, charged, slow e twice hanno tutti 2. L'ordine alfabetico ha deciso chi ha preso il quinto posto, quindi twice è stata scartata. La regola non è perfetta — twice non è stata scartata perché meno importante — ma è dichiarata ed è la stessa ogni volta. Se qualcuno che legge il report lo chiede, c'è una risposta.
Input vuoto gestito apposta. max() su una collezione vuota solleva ValueError: max() iterable argument is empty. Una settimana tranquilla senza ticket è normale, non un errore — quindi print_ranking controlla prima se la classifica è vuota. La seconda riga di output mostra proprio questo.
Conteggio e visualizzazione separati. top_words restituisce una lista e non stampa nulla; print_ranking solo stampa. Così gli stessi conteggi possono essere usati da un test, un'API o una dashboard. {word:<{width}} allinea a sinistra ogni parola in uno spazio largo quanto la più lunga, così la colonna dei conteggi resta allineata.
Un limite: charged e charge qui sono due parole separate. Unire le forme di una parola è un problema a sé, e i sistemi AI di solito confrontano per significato invece che contando le parole — un approccio che arriverà più avanti nel corso. Ma anche lì, pulire il testo, filtrare e ordinare in modo stabile — il lavoro di questo capitolo — sono i primi passi.
Quando si rompe
KeyError: 'refund' counts[word] += 1 per una chiave che non è ancora nel dizionario. Scrivi counts[word] = counts.get(word, 0) + 1, oppure usa collections.Counter.
ValueError: too many values to unpack (expected 2) Il codice scrive for key, value in d: su un dizionario. Scorrere direttamente un dizionario dà solo le chiavi; scrivi d.items() per ottenere le coppie.
TypeError: unhashable type: 'list' Una lista è stata messa in un set o usata come chiave di dizionario. Gli elementi di un set e le chiavi di un dizionario devono essere cose che non possono cambiare. Usa una tupla al posto della lista: seen.add(("refund", "late")).
RuntimeError: dictionary changed size during iteration Sono state cancellate o aggiunte chiavi a un dizionario mentre lo si scorreva. Costruisci un nuovo dizionario invece di cancellare: counts = {word: n for word, n in counts.items() if n >= 2}.
AttributeError: 'tuple' object has no attribute 'append' Una tupla non può essere modificata dopo la creazione. Se i valori davvero crescono e diminuiscono, doveva essere una lista; se la tupla è un record, costruisci una nuova tupla con il nuovo valore.
ValueError: max() iterable argument is empty max() o min() su una collezione vuota. Controlla prima se è vuota, oppure dai un default: max(lengths, default=0).
Passo 4 di 6 — Prevedi
Verifica cosa hai capito
Un collega sta prendendo «la parola più frequente» da un dizionario di conteggi. Cosa viene stampato?
counts = {"refund": 5, "slow": 1, "login": 3}
print(sorted(counts, reverse=True)[0])- A`refund`
- B`('refund', 5)`
- C`slow`
- D`5`
label(2600) dovrebbe restituire "critical", ma restituisce "slow" — senza nessun errore. Dov'è il bug?
def label(latency_ms):
if latency_ms >= 1000:
return "slow"
elif latency_ms >= 2000:
return "critical"
return "ok"
print(label(2600))- AL'ordine delle condizioni: `>= 1000` è vera per prima e la catena si ferma, quindi `>= 2000` non viene mai raggiunta — la condizione più stringente deve venire per prima
- B`return "ok"` ha bisogno di un `else:` prima
- CIl secondo controllo deve usare `if` invece di `elif`
- D`2600` è un intero, quindi va convertito con `float()` prima del confronto con `>=`
Una pipeline controlla, per ognuna di un milione di parole, se è una delle 300 stop words. Quale contenitore deve contenere le stop words?
- AUna stringa, con tutte le parole unite in un unico testo lungo
- BUna tupla, perché le stop words non cambiano
- CUn set, perché un controllo `in` è quasi istantaneo, qualunque sia la sua dimensione
- DUna lista, perché può essere tenuta in ordine alfabetico
Giocare fa parte di Pro
+30 XP ti aspettano
Gioca questo capitolo con Pro
Leggere è gratis e l’intero capitolo è aperto. I primi 5 capitoli di ogni corso gratuito si giocano gratis. Dal capitolo 6, rispondere alle domande, guadagnare XP e far crescere la serie fa parte di Pro.
Sblocca il gioco con ProTocca a te
Ora trova le parole più frequenti per categoria. In un file chiamato categories.py, copia STOP_WORDS e tokenize del capitolo, e aggiungi questi ticket:
TICKETS = [
("billing", "Charged twice for one order."),
("billing", "Refund for the double charge, please."),
("login", "Login fails on the new phone."),
("billing", "Still no refund after the charge."),
("login", "Password reset email never arrives."),
("login", "Login page shows a blank screen."),
]Poi scrivi il codice in modo che:
- Ogni categoria abbia i suoi conteggi — un dizionario le cui chiavi sono i nomi delle categorie e i cui valori sono il
Counterdi quella categoria (suggerimento:dict.setdefaulteCounter.update) - Le stop words e le parole di due lettere o meno non vengano contate
- Ogni categoria mostri le sue prime due parole, con il conteggio più alto per primo e ordine alfabetico in caso di parità
- Le categorie siano stampate in ordine alfabetico, qualunque sia l'ordine in cui arrivano i ticket
uv run categories.py dovrebbe stampare esattamente questo:
billing charge=2, refund=2
login login=2, arrives=1Poi inverti la lista TICKETS (TICKETS[::-1]) ed eseguilo di nuovo — l'output cambia? Se cambia, quale condizione non è rispettata? Infine, togli lo spareggio e ordina solo per conteggio: qual è la seconda parola per login, e resta la stessa quando inverti l'ordine?
Passo 6 di 6
Sfida — il quiz del capitolo
Dieci domande dalla più facile alla più difficile. Le ultime sono difficili di proposito.
Gioca con Pro