Python per i dati, parte 1: ambiente isolato e fondamenti del linguaggio
Corso Machine Learning Engineer · Modulo 01 · Python per i dati · Lezione 1
Obiettivo. Mettere il lettore nelle condizioni di scrivere e far girare Python in un ambiente riproducibile, con un modello mentale corretto di cosa succede quando scrive b = a. Alla fine sa creare un ambiente virtuale, installare pacchetti senza sporcare il sistema, e usare variabili, numeri, stringhe, condizioni e cicli sapendo dove sono le trappole.
Falso mito. «Una variabile è una scatola che contiene un valore.» In Python una variabile è un'etichetta attaccata a un oggetto. Due etichette possono stare sullo stesso oggetto, ed è la sorgente del bug più comune dei principianti.
Introduzione
Python è il linguaggio in cui si fa machine learning: le librerie che useremo (NumPy, Pandas, scikit-learn, e più avanti i framework di reti neurali) sono scritte per essere chiamate da Python. Non servirà conoscerne ogni angolo: servirà conoscere bene la parte che serve ai dati. Questa lezione la fonda.
Roadmap. (1) Perché esistono gli ambienti virtuali e come si usano. (2) Nomi e oggetti. (3) Numeri e operazioni, con la divisione intera dimostrata. (4) Verità e condizioni. (5) Cicli. (6) Errori da principiante.
Cos'è, perché esiste, quando NON usarlo
Python è un linguaggio interpretato, a tipizzazione dinamica: scrivi, esegui, e il tipo dei valori si decide mentre il programma gira. Il vantaggio è la velocità con cui si prova un'idea. Il prezzo è che molti errori compaiono solo all'esecuzione, e che i cicli in puro Python sono lenti rispetto a codice compilato: per questo i dati si elaborano con NumPy, che sposta il lavoro in codice compilato (lezione 05).
Quando Python NON è la scelta giusta: calcolo intensivo scritto a mano senza librerie (lo scrivi in un linguaggio compilato o usi NumPy); sistemi con requisiti rigidi di latenza o di memoria; applicazioni mobili native. Python come «colla» che orchestra librerie veloci rimane imbattibile per i dati.
Installazione, ambiente isolato, verifica
Analogia: la cassetta degli attrezzi per progetto
Immagina di fare due lavori: uno richiede una vecchia versione di un attrezzo, l'altro la nuova. Se hai un'unica cassetta condivisa, un lavoro rompe l'altro. L'ambiente virtuale è una cassetta per ciascun progetto: cartella con una copia dell'interprete e dei pacchetti, isolata dal resto del sistema.
Perché è obbligatorio, non facoltativo
Sulle distribuzioni Linux recenti installare pacchetti nel Python di sistema con pip viene rifiutato, con l'errore externally-managed-environment: è la PEP 668, che protegge il sistema da pacchetti in conflitto con quelli gestiti dal gestore di pacchetti della distribuzione. L'ambiente virtuale è la risposta prevista.
python3 --version # verifica: 3.12 o successivo
python3 -m venv .venv # crea l'ambiente nella cartella .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
python -m pip install --upgrade pip
python -m pip install numpy pandas matplotlib scikit-learn
python -m pip freeze > requirements.txt # fotografia delle versioni installate
python -c "import numpy, pandas, sklearn; print(numpy.__version__, pandas.__version__, sklearn.__version__)"
Tre abitudini che evitano ore di guai:
python -m pip, nonpip: garantisce che il pacchetto vada nell'interprete che stai usando.- Un ambiente per progetto, cartella
.venvesclusa dal controllo di versione. requirements.txt(o equivalente) versionato: chi arriva dopo di te ricrea lo stesso ambiente conpython -m pip install -r requirements.txt.
Le tre abitudini risolvono tre guasti diversi: il primo è il pacchetto installato nell'interprete sbagliato, il secondo è il conflitto tra progetti, il terzo è la non riproducibilità. Sono ordinate per frequenza di incidente: la prima ti salva già nella prima settimana, l'ultima quando qualcun altro prova a rifare il tuo lavoro.
Che cosa fa davvero il comando python3 -m venv .venv. Crea una cartella .venv che contiene una copia (o un collegamento) dell'interprete Python, una cartella site-packages vuota in cui finiranno i pacchetti installati, e un file di configurazione pyvenv.cfg che ricorda da quale Python è stata derivata. Il comando activate non «installa» nulla: modifica la variabile d'ambiente PATH della tua shell, in modo che digitando python si trovi per primo quello dentro .venv. Da qui nasce la verifica con which python. Dentro Python puoi controllare da solo in quale ambiente ti trovi:
import sys
print("versione di Python:", sys.version_info[:3])
print("sei dentro un ambiente virtuale?", sys.prefix != sys.base_prefix)
# Output (fuori da un ambiente virtuale, Python 3.14.4):
# versione di Python: (3, 14, 4)
# sei dentro un ambiente virtuale? False
Output ottenuto:
versione di Python: (3, 14, 4)
sei dentro un ambiente virtuale? False
Come leggere il codice. sys.prefix è la radice dell'installazione che sta eseguendo il programma; sys.base_prefix è la radice dell'installazione di base da cui l'ambiente è stato creato. Dentro un ambiente virtuale i due percorsi differiscono; fuori sono uguali. Qui il blocco è stato eseguito fuori da un ambiente, quindi la risposta è False: se lo esegui dopo activate dovrebbe diventare True. È un controllo che puoi mettere all'inizio di uno script per rifiutare di partire nell'ambiente sbagliato.
Verifica che funzioni: dopo activate, which python (o where python su Windows) deve puntare dentro .venv. Se punta altrove, stai usando il Python sbagliato.
Riproducibilità, in pratica. pip freeze scrive l'elenco di tutti i pacchetti installati con la versione esatta (numpy==2.3.5), anche quelli che hai installato solo indirettamente come dipendenze. Va bene per ricreare uno stesso ambiente identico (per esempio su un altro computer o su un server); per un progetto che deve restare leggibile, molti tengono anche un elenco breve dei soli pacchetti scelti da loro. Il punto è che i numeri prodotti da un'analisi dipendono dalle versioni delle librerie: scrivere nel report «risultati ottenuti con NumPy 2.3 e scikit-learn 1.8» non è pignoleria, è ciò che permette a chi legge di ripetere l'esperimento e trovare gli stessi numeri. Lo faremo in tutte le lezioni.
Primo esempio: nomi e oggetti
a = [1, 2, 3]
b = a # NON copia: attacca una seconda etichetta allo stesso oggetto
b.append(4)
print(a, a is b) # a è cambiata, anche se abbiamo "toccato" solo b
c = list(a) # copia (superficiale): un nuovo oggetto con gli stessi elementi
print(c is a, c == a)
# Output:
# [1, 2, 3, 4] True
# False True
Output: [1, 2, 3, 4] True e False True.
Un secondo esempio, contrastante: nomi, funzioni e modifiche sul posto
Il b = a di prima mostra due nomi sullo stesso oggetto. Lo stesso accade, in modo meno visibile, quando passi un oggetto a una funzione: il parametro è un nuovo nome per l'oggetto che hai passato. Ciò che succede dentro la funzione dipende da una distinzione sola: la funzione modifica l'oggetto, oppure ricollega il nome a un oggetto nuovo?
def aggiungi_in_coda(lista, valore):
lista.append(valore) # MODIFICA l'oggetto ricevuto: chi ha passato la lista lo vedrà
def con_nuova_lista(lista):
lista = lista + [99] # crea un oggetto NUOVO e lo lega al nome locale
return lista
dati = [1, 2, 3]
aggiungi_in_coda(dati, 4)
print("dopo aggiungi_in_coda:", dati)
nuova = con_nuova_lista(dati)
print("dati:", dati, "| nuova:", nuova, "| sono lo stesso oggetto?", nuova is dati)
# il caso dei numeri: immutabili, l'operatore += crea un nuovo oggetto
x = 10
y = x
y += 1
print("x =", x, "| y =", y)
# il caso contrastante delle liste: += modifica sul posto
a = [1, 2]
b = a
b += [3]
print("a =", a, "| b =", b, "| stesso oggetto?", a is b)
Output ottenuto:
dopo aggiungi_in_coda: [1, 2, 3, 4]
dati: [1, 2, 3, 4] | nuova: [1, 2, 3, 4, 99] | sono lo stesso oggetto? False
x = 10 | y = 11
a = [1, 2, 3] | b = [1, 2, 3] | stesso oggetto? True
Come leggere il codice. La prima funzione chiama append, un metodo che modifica la lista: dopo la chiamata dati contiene anche il 4, perché la funzione e il chiamante condividono lo stesso oggetto. La seconda scrive lista = lista + [99]: l'operatore + fra liste costruisce una lista nuova, e l'assegnazione ricollega il nome locale lista a quella nuova; dati resta com'era e nuova è un altro oggetto (is dà False). Il caso dei numeri completa il quadro: un int non si può modificare (è immutabile), quindi y += 1 significa y = y + 1, cioè un numero nuovo legato a y; x non cambia. Il caso finale è la trappola: per le liste b += [3] non crea una lista nuova ma modifica quella esistente, e poiché a e b sono lo stesso oggetto cambiano entrambe. La stessa riga += ha significati diversi a seconda che l'oggetto sia mutabile o no.
La regola pratica. Una funzione che riceve un oggetto mutabile (lista, dizionario, array) e lo modifica sul posto ha un effetto collaterale visibile fuori. Se non lo vuoi, lavora su una copia (copia = list(lista)) oppure restituisci un oggetto nuovo e documentalo. Le funzioni «pure» (che calcolano un risultato senza modificare i loro argomenti) sono più facili da testare e da capire, ed è lo stile che useremo per le trasformazioni di dati.
Il modello mentale
Ogni oggetto in Python ha tre proprietà (documentazione del data model): identità (non cambia mai, è un numero unico mentre l'oggetto vive), tipo, valore. L'istruzione b = a crea un legame del nome b con l'oggetto già indicato da a. Due nomi, un oggetto.
isconfronta l'identità: «sono lo stesso oggetto?».==confronta il valore: «sono uguali?».
Due liste con gli stessi elementi sono == ma non is: hanno lo stesso contenuto ma sono due oggetti distinti, con due identità. Il confronto di valore è quello che vuoi quasi sempre; il confronto di identità serve per sapere se due nomi puntano allo stesso oggetto, per esempio per scoprire un alias non voluto.
Trappola reale: scrivere if x is 1 invece di if x == 1. Funziona per caso con i numeri piccoli in certe implementazioni, e fallisce altrove; l'interprete moderno emette un SyntaxWarning proprio per questo. Usa is solo per None (if x is None). Su Python 3.14.4 l'espressione x is 1 eseguita con -W error fallisce con SyntaxError: "is" with 'int' literal. Did you mean "=="?: l'avviso è un SyntaxWarning emesso già in compilazione.
Perché conta nei dati: passare un array a una funzione che lo modifica «sul posto» altera anche l'originale. Lo ritroveremo con NumPy (viste contro copie) e Pandas.
Leggere un errore: i messaggi di Python sono informazione
Chi comincia tende a spaventarsi quando compare una «schermata rossa». In realtà un messaggio di errore di Python è un'informazione molto precisa: dice che tipo di errore è, che cosa lo ha causato e dove (il traceback elenca le chiamate, dall'esterno all'interno, fino alla riga incriminata). Si legge dal fondo: l'ultima riga dà il tipo e il messaggio. Ecco i sei errori che incontrerai più spesso, provocati apposta e intercettati:
casi = {
"NameError": lambda: variabile_mai_definita,
"TypeError": lambda: "3" + 4,
"IndexError": lambda: [1, 2, 3][5],
"KeyError": lambda: {"a": 1}["b"],
"ValueError": lambda: int("12x"),
"ZeroDivisionError": lambda: 1 / 0,
}
for nome, prova in casi.items():
try:
prova()
except Exception as errore:
print(f"{nome:<18} -> {errore}")
Output ottenuto:
NameError -> name 'variabile_mai_definita' is not defined
TypeError -> can only concatenate str (not "int") to str
IndexError -> list index out of range
KeyError -> 'b'
ValueError -> invalid literal for int() with base 10: '12x'
ZeroDivisionError -> division by zero
Come leggere il codice. casi è un dizionario che associa il nome atteso dell'errore a una funzione anonima (lambda) che lo provoca. Il ciclo esegue ciascuna dentro un blocco try/except: se la funzione solleva un'eccezione, l'esecuzione salta a except, che stampa il messaggio. Il significato di ciascuno: NameError = hai usato un nome che non esiste (di solito un refuso o un'assegnazione mancata); TypeError = hai combinato tipi incompatibili (qui un testo e un numero); IndexError = hai chiesto una posizione oltre la fine; KeyError = hai chiesto una chiave che il dizionario non ha; ValueError = il tipo è giusto ma il valore no ("12x" non è un intero); ZeroDivisionError = divisione per zero. Imparare a riconoscere il tipo restringe subito dove cercare: un KeyError si cerca nei dizionari, un IndexError nelle liste.
Esercizio 1
Due etichette, un solo registro
Una stazione meteo tiene le letture in decimi di grado nella lista letture = [118, 124, 131]. Un collega scrive copia = letture e poi copia.append(145). Rispondi carta e penna, senza eseguire nulla.
- Quanto vale
len(letture)dopo le due righe? - Che cosa restituiscono
letture is copiaeletture == copia? - Il collega voleva una copia indipendente. Quale riga avrebbe dovuto scrivere, e che cosa restituirebbero allora le due domande del punto 2?
Le tre domande verificano il modello mentale della lezione: un nome è un'etichetta, una modifica sul posto si vede da tutte le etichette, una copia vera crea un oggetto nuovo. Se rispondi «3» alla prima, rileggi il blocco con b = a e disegna le etichette su un foglio.
Mostra la soluzione
1. 4. copia = letture non copia niente: attacca una seconda etichetta allo stesso oggetto. L'append modifica quell'unico oggetto, e la lista vista attraverso il nome letture contiene ora [118, 124, 131, 145].
2. Entrambe True. is confronta l'identità e i due nomi indicano lo stesso oggetto; == confronta il valore, e il valore è per forza lo stesso, visto che l'oggetto è uno solo.
3. copia = list(letture) (oppure letture.copy()). Da quel momento letture is copia vale False — sono due oggetti distinti — mentre letture == copia vale True finché nessuno dei due viene modificato. È la distinzione che conta: due liste con gli stessi elementi sono uguali ma non sono la stessa lista.
Teoria: i numeri e il loro comportamento
Interi senza limite, decimali con limite
Gli int di Python hanno precisione arbitraria: la dimensione è limitata solo dalla memoria.
x = 10**30 + 1
print(x, type(x))
print(7 / 2, type(7 / 2)) # divisione "vera": sempre float
print(7 // 2, 7 % 3) # divisione intera e resto
# Output:
# 1000000000000000000000000000001
# 3.5
# 3 1
Output: 1000000000000000000000000000001 <class 'int'>; 3.5 <class 'float'>; 3 1.
I float hanno invece precisione finita (64 bit): la lezione 04 ne spiega la struttura e le conseguenze. Per ora ricorda: 0.1 + 0.2 == 0.3 vale False.
Divisione intera e resto: dimostrazione di cosa fanno con i negativi
Con i numeri positivi nessuna sorpresa: 7 // 2 = 3, 7 % 2 = 1. Con i negativi, molti linguaggi danno risposte diverse, e il principiante resta spiazzato. Python definisce:
a // b è il pavimento di a / b (l'intero più grande ≤ al quoziente reale), e il resto a % b è definito di conseguenza per mantenere l'identità
a = (a // b) · b + (a % b).
Verifica con a = −7, b = 3:
−7 / 3 = −2.333…, il pavimento è−3. Quindi−7 // 3 = −3.- Il resto deve soddisfare
−7 = (−3)·3 + r, cioèr = −7 + 9 = 2. Quindi−7 % 3 = 2.
Il resto ha sempre il segno del divisore (qui positivo). Questa scelta rende % utile per «avvolgere» indici: indice % lunghezza è sempre fra 0 e lunghezza − 1, anche con indici negativi.
print(-7 // 3, -7 % 3, divmod(-7, 3))
a, b = -7, 3
print(a == (a // b) * b + (a % b))
# Output:
# -3 2 (-3, 2)
# True
Output: -3 2 (-3, 2) e True.
Esercizio 2
Le 21:30 del giorno prima
La stazione registra ogni lettura come numero di minuti dalla mezzanotte. La centrale chiede il momento che sta 150 minuti prima della mezzanotte, cioè -150. Tre operatori — Anna, Berto, Carla — si alternano: la lettura 0 è di Anna, la 1 di Berto, la 2 di Carla.
- Quanto valgono
-150 // 60e-150 % 60? Verifica che l'identitàa == (a // b) * b + (a % b)regga. - Che ora è, scritta come
HH:MM? (Suggerimento: riporta prima il minuto dentro la giornata con% 1440.) - Chi ha fatto la lettura numero
-1, cioè l'ultima di ieri?
L'esercizio usa la proprietà più utile del resto di Python: con un divisore positivo il risultato cade sempre tra 0 e il divisore meno uno. Per questo lo stesso operatore serve per le ore del giorno, i turni a rotazione e gli indici circolari, senza distinguere il segno.
Mostra la soluzione
1. -150 / 60 vale -2.5; // è il pavimento, cioè l'intero più grande minore o uguale, quindi -150 // 60 = -3 (non -2). Il resto segue per definizione: -150 - (-3) * 60 = -150 + 180 = 30, quindi -150 % 60 = 30. Verifica: (-3) * 60 + 30 = -150. Il resto ha il segno del divisore, e infatti è positivo.
2. -150 % 1440 = 1290, e 1290 = 21 * 60 + 30: sono le 21:30 del giorno prima. Nessun if sul segno: ci pensa la definizione di %. Attenzione a non usare int(-150 / 1440), che tronca verso lo zero e darebbe 0 invece di -1, perdendo il «giorno prima».
3. Carla: -1 % 3 vale 2, non -1, sempre per la stessa regola del segno. È il motivo per cui operatori[indice % len(operatori)] è corretto per costruzione e non per fortuna, anche con indici negativi grandi come -7.
Stringhe e formattazione
Le stringhe sono immutabili: ogni operazione ne crea una nuova. La formattazione moderna usa le f-string, che valutano espressioni tra parentesi graffe e accettano specifiche di formato dopo i due punti.
nome, ricavo = "Marta", 1234567.891
print(f"{nome.upper()} ha fatturato {ricavo:,.2f} euro") # separatore migliaia, 2 decimali
print(f"{0.4567:.1%}") # percentuale
# Output:
# MARTA ha fatturato 1,234,567.89 euro
# 45.7%
Output: MARTA ha fatturato 1,234,567.89 euro e 45.7%. (Il separatore delle migliaia è la virgola: è convenzione del formato, indipendente dalla lingua del sistema.)
Codice: condizioni e cicli
Verità dei valori
In una condizione, Python considera «falso» (falsy): False, None, zero (0, 0.0), contenitori vuoti ("", [], {}, set()). Tutto il resto è vero. Quindi if lista: significa «se la lista non è vuota».
print(bool(0), bool(""), bool([]), bool([0]), bool(None))
# Output:
# False False False True False
Output: False False False True False. Nota [0]: una lista con un elemento (che vale zero) è comunque non vuota, quindi vera.
Condizioni
def classifica_voto(v):
"""Problema: tradurre un voto 0-30 in una fascia. Edge case: voti fuori scala."""
if not 0 <= v <= 30: # confronto concatenato: leggibile come in matematica
raise ValueError(f"voto fuori scala: {v}")
if v >= 28:
return "ottimo"
elif v >= 24:
return "buono"
elif v >= 18:
return "sufficiente"
return "insufficiente"
print([classifica_voto(v) for v in (30, 26, 18, 17)])
# Output:
# ['ottimo', 'buono', 'sufficiente', 'insufficiente']
Output: ['ottimo', 'buono', 'sufficiente', 'insufficiente'].
Punti di stile e correttezza: l'ordine dei rami è importante (dal più restrittivo); un ramo raise all'inizio rifiuta presto gli input non validi («fail fast»); il confronto 0 <= v <= 30 è valido in Python e significa 0 <= v and v <= 30.
Cicli for
for itera su qualsiasi oggetto iterabile. Due utilità che userai ovunque: enumerate (coppie indice-elemento) e zip (iterare più sequenze in parallelo).
nomi = ["Anna", "Luca"]
voti = [30, 21]
for posizione, (nome, voto) in enumerate(zip(nomi, voti), start=1):
print(posizione, nome, voto)
# Output:
# 1 Anna 30
# 2 Luca 21
Output: 1 Anna 30 e 2 Luca 21.
Trappola: zip si ferma alla sequenza più corta, in silenzio. Se nomi ha 100 elementi e voti ne ha 99, perdi una riga senza errori. Per i dati allineati usa zip(a, b, strict=True), che solleva un errore se le lunghezze differiscono.
Come leggere i due cicli precedenti. Nel primo, enumerate(zip(nomi, voti), start=1) produce coppie (posizione, (nome, voto)): zip accoppia i due elenchi elemento per elemento, enumerate aggiunge un contatore che parte da 1, e la scrittura posizione, (nome, voto) scompatta la struttura annidata in tre nomi. Un ciclo for si legge sempre così: «per ogni elemento dell'iterabile, esegui il corpo con il nome legato a quell'elemento». Il vantaggio rispetto a for i in range(len(...)) è che non maneggi indici a mano, e quindi non puoi sbagliarli.
Cicli while
while ripete finché la condizione è vera. Si usa quando non sai in anticipo quante iterazioni servono. Esempio: la successione di Collatz (se pari dividi per 2, se dispari moltiplica per 3 e somma 1) dal numero 27.
n, passi = 27, 0
while n != 1:
n = n // 2 if n % 2 == 0 else 3 * n + 1
passi += 1
print(passi)
# Output:
# 111
Output: 111. Il ciclo termina per 27, ma che termini per ogni intero positivo è un problema matematico aperto (congettura di Collatz): è un buon promemoria che un while richiede di ragionare sulla terminazione.
Un ciclo con un criterio di arresto numerico
Il while è lo strumento naturale quando si ripete finché una quantità converge. Calcoliamo una radice quadrata con un metodo iterativo (il metodo di Newton applicato a x² = n: si parte da una stima x e la si migliora con x ← (x + n/x)/2; la giustificazione la vedrai nella lezione di analisi). Si ferma quando due stime successive differiscono meno di una tolleranza, e c'è sempre un tetto al numero di passi, perché senza tetto un errore di ragionamento diventa un ciclo infinito.
import math
def radice(n, tolleranza=1e-12, tetto=60):
x = n if n > 1 else 1.0 # stima di partenza
for passo in range(1, tetto + 1):
nuovo = 0.5 * (x + n / x)
if abs(nuovo - x) <= tolleranza * max(1.0, abs(nuovo)):
return nuovo, passo
x = nuovo
raise RuntimeError("il metodo non converge entro il tetto di passi")
for n in (2, 9, 1e6, 0.0001):
valore, passi = radice(n)
print(f"radice({n}) = {valore:.12g} in {passi} passi | math.sqrt = {math.sqrt(n):.12g}")
Output ottenuto:
radice(2) = 1.41421356237 in 6 passi | math.sqrt = 1.41421356237
radice(9) = 3 in 7 passi | math.sqrt = 3
radice(1000000.0) = 1000 in 15 passi | math.sqrt = 1000
radice(0.0001) = 0.01 in 12 passi | math.sqrt = 0.01
Come leggere il codice. La stima iniziale è n (o 1 se n ≤ 1). A ogni passo si calcola la media tra x e n/x: se x è troppo grande, n/x è troppo piccolo, e la media si avvicina al valore giusto. Il test d'arresto confronta due stime successive in modo relativo (max(1, |nuovo|) evita che per numeri molto grandi o molto piccoli la tolleranza assoluta diventi inadatta: lezione 4 sui numeri in virgola mobile). Il metodo converge in pochi passi (da 6 a 15 per questi valori: dipende da quanto la stima di partenza è lontana) perché, una volta vicino alla soluzione, a ogni iterazione il numero di cifre corrette circa raddoppia. Il raise finale è la rete di sicurezza: se per un valore patologico non converge, il programma lo dice invece di girare per sempre.
Regola: ogni while deve avere un'azione dentro il corpo che avvicina la condizione alla falsità, altrimenti il programma gira all'infinito.
Somma dei primi n interi: tre modi, un teorema
n = 100
somma_ciclo = 0
for k in range(1, n + 1):
somma_ciclo += k
print(somma_ciclo, sum(range(1, n + 1)), n * (n + 1) // 2)
# Output:
# 5050 5050 5050
Output: 5050 5050 5050. Il terzo metodo è la formula di Gauss, con una dimostrazione da due righe: scrivi la somma S = 1 + 2 + … + n e di nuovo al contrario S = n + (n−1) + … + 1. Sommando colonna per colonna, ogni coppia vale n + 1, e ci sono n coppie: 2S = n(n+1), quindi S = n(n+1)/2. Il ciclo costa n operazioni, la formula una sola: un'idea matematica batte un ciclo. È lo spirito di tutto il corso, e il motivo per cui NumPy, che esprime il calcolo su interi vettori, è più veloce.
La soluzione commentata del mini-progetto
Per chiudere mettiamo insieme quasi tutto: condizioni, cicli, eccezioni, dizionari e un po' di stile. Ecco una possibile soluzione del mini-progetto, con i tre test.
def classifica_voto(v):
if not 0 <= v <= 30:
raise ValueError(f"voto fuori scala: {v}")
return v
def statistiche_voti(voti):
validi, scartati = [], 0
for v in voti:
try:
validi.append(classifica_voto(v))
except ValueError:
scartati += 1
if not validi: # lista vuota: nessuna media definibile
return {"validi": 0, "media": None, "minimo": None, "massimo": None, "scartati": scartati}
return {
"validi": len(validi),
"media": sum(validi) / len(validi),
"minimo": min(validi),
"massimo": max(validi),
"scartati": scartati,
}
assert statistiche_voti([30, 24, 18]) == {"validi": 3, "media": 24.0, "minimo": 18, "massimo": 30, "scartati": 0}
assert statistiche_voti([30, 99, -1, 20])["scartati"] == 2
assert statistiche_voti([])["media"] is None
print(statistiche_voti([30, 99, -1, 20, 27]))
print("test superati")
Output ottenuto:
{'validi': 3, 'media': 25.666666666666668, 'minimo': 20, 'massimo': 30, 'scartati': 2}
test superati
Come leggere la soluzione. classifica_voto ripete il controllo di validità e solleva ValueError per i voti fuori scala. statistiche_voti scorre i voti; per ciascuno prova ad accettarlo dentro un try: se la funzione solleva un ValueError, l'errore viene intercettato e viene incrementato il contatore degli scartati, senza che il programma si fermi. Il caso della lista vuota è deciso esplicitamente: la media di zero numeri non esiste, e restituire None (e non, per esempio, 0) è onesto, perché uno zero sarebbe un valore falso che potrebbe finire in un calcolo successivo. I tre assert sono i test: se uno fallisce, Python solleva AssertionError indicando quale. Scrivere i test prima o subito dopo la funzione è la prima forma di verifica; nelle lezioni successive la ritroverai come abitudine costante.
Un vocabolario minimo
- Interprete: il programma che legge ed esegue il codice Python.
- Ambiente virtuale: cartella isolata con un proprio interprete e propri pacchetti.
- Oggetto / nome: un valore con identità, tipo e valore / l'etichetta che lo designa.
- Mutabile / immutabile: oggetto che si può / non si può modificare sul posto.
- Effetto collaterale: modifica visibile fuori dalla funzione che l'ha causata.
- Iterabile: oggetto da cui un ciclo
forpuò prendere gli elementi uno alla volta. - Traceback: elenco delle chiamate che hanno portato a un errore.
- Falsy / truthy: valori considerati falsi / veri in una condizione.
- f-string: stringa con espressioni tra graffe, valutate al momento.
Le nove voci si dividono in tre blocchi: l'ambiente (interprete, ambiente virtuale), il modello dei dati (oggetto e nome, mutabile, effetto collaterale, iterabile) e il leggere gli errori (traceback, falsy, f-string). Il blocco centrale è quello che la lezione ripete: quasi ogni errore di un principiante sui dati viene da un alias o da una modifica sul posto non voluti.
Errori tipici (sintomo → causa → rimedio)
| Errore | Sintomo | Causa | Rimedio |
|---|---|---|---|
externally-managed-environment |
pip rifiuta di installare |
PEP 668: stai usando il Python di sistema | Crea e attiva un ambiente virtuale |
ModuleNotFoundError dopo aver installato |
Il pacchetto «c'è» ma non si importa | Hai installato in un interprete diverso da quello che esegue il programma | python -m pip install ... e controlla which python |
| Lista modificata «misteriosamente» | Un dato cambia in un punto lontano | Due nomi sullo stesso oggetto | list(x) o x.copy() quando serve una copia |
is al posto di == |
Funziona su un caso, fallisce su un altro | Identità invece di valore | == per i valori, is solo per None |
| Righe perse | Dataset più corto del previsto | zip si ferma alla più corta |
zip(..., strict=True) |
| Ciclo infinito | Il programma non termina | La condizione non cambia nel corpo | Stampa/traccia la variabile di controllo; aggiungi un tetto |
IndentationError |
Il codice non parte | Python usa l'indentazione come sintassi | 4 spazi, mai mescolati con tab |
Best practice
- Segui PEP 8: nomi in
snake_case, righe leggibili, spazi attorno agli operatori. - Nomi che dicono cosa contengono (
ricavo_netto, nonx2). - Un ambiente per progetto;
requirements.txtversionato. - Messaggi di errore con il valore incriminato (
f"voto fuori scala: {v}"). - Leggi PEP 20 (
import this): «Gli errori non dovrebbero mai passare in silenzio». - Preferisci le funzioni integrate (
sum,min,max,sorted) ai cicli scritti a mano.
Le prime tre regole riguardano l'ordine del progetto, la quarta e la quinta il modo di sbagliare in modo utile, l'ultima la velocità e la leggibilità. Quella che cambia di più la qualità dei programmi dei principianti è il messaggio d'errore con il valore incriminato: trasforma «qualcosa non va» in «il voto 31 è fuori scala».
Mini-progetto
Scrivi statistiche_voti.py che, data una lista di voti (anche con valori non validi), restituisce un dizionario con numero di voti validi, media, minimo, massimo e numero di scartati.
- Valida ogni voto con
classifica_voto-like logic; scarta (e conta) quelli fuori da 0-30 intercettandoValueError. - Calcola la media come
somma / quanti, gestendo il caso lista vuota (che cosa deve restituire?). - Aggiungi tre
assertcome test: lista normale, lista con scartati, lista vuota.
I tre punti vanno dal più meccanico al più delicato: la validazione riusa l'eccezione sollevata dalla funzione di classificazione, la media chiede di decidere che cosa significa «nessun dato», i test fissano le decisioni. La soluzione commentata più sopra mostra una scelta (restituire None), ma non è l'unica difendibile: l'importante è che sia esplicita.
Challenge
- Easy. Stampa i multipli di 7 minori di 100 con un solo ciclo
forerange. - Medium. Scrivi
e_primo(n)con un ciclo fino aint(n**0.5). Perché basta arrivare alla radice quadrata? (Sen = a·bcona ≤ b, alloraa ≤ √n.) - Hard. Implementa il massimo comun divisore con l'algoritmo di Euclide in un
while, e dimostra la terminazione: quale quantità decresce a ogni passo? - Expert. Mostra con un esempio numerico che
(a // b) * b + a % b == aper 1000 coppie casuali con segni misti. Poi spiega perchéint(a / b)non è equivalente aa // bper i negativi. - Enterprise. Prepara per un team un template di progetto: struttura cartelle,
.gitignoreper.venv,requirements.txt, una paginaREADMEcon i tre comandi per ricreare l'ambiente.
Le prime due sfide sono di calcolo, le ultime tre di ragionamento: Hard chiede di dimostrare la terminazione e Expert di spiegare la differenza fra pavimento e troncamento, che è proprio la trappola dell'esercizio 2. Se ne scegli una, scegli Expert: ripete il punto più contro-intuitivo della lezione con numeri tuoi.
Codice di questa lezione
Tre esercizi di codice sulla stazione meteo simulata della lezione, con le parti da completare e, in fondo, le soluzioni commentate. Richiede solo Python 3.12+ e la libreria standard.
Eseguilo con python3 esercizi.py: gira anche con le parti non completate e ti dice quali mancano; il file è nella cartella di download del sito.
Recap
Mappa mentale. Ambiente virtuale = cassetta per progetto. Variabile = etichetta su un oggetto. is = identità, == = valore. // = pavimento, % = resto col segno del divisore. Falsy = zero, vuoto, None. for per sequenze note, while quando serve una condizione, con terminazione dimostrata.
3 concetti: ambiente isolato; nomi e oggetti; verità dei valori.
3 errori: pip nel sistema; alias che modificano; zip che tronca.
3 best practice: PEP 8; requirements.txt; fail fast con messaggi chiari.
1 esercizio: il mini-progetto di questa lezione.
Prossima lezione: liste, tuple, dizionari e insiemi: le strutture in cui tieni i dati.
Nella stessa serie trovi anche: 07 - Cloud e intelligenza artificiale (la lezione precedente), 09 - Python per i dati 2: liste, tuple, dizionari, insiemi (la lezione successiva).
Fonti
- PEP 668: Marking Python base environments as externally managed - il file marcatore
EXTERNALLY-MANAGEDe il rifiuto delle installazioni fuori da un ambiente virtuale, consultata il 2026-11-12 - Python: venv, creation of virtual environments - la cartella creata da
python -m venv, il filepyvenv.cfge la sottocartella con l'interprete, consultata il 2026-11-12 - Python Language Reference: Data model - «ogni oggetto ha un'identità, un tipo e un valore» e il significato di
is, consultata il 2026-11-12 - Python: funzioni integrate -
zipe l'opzionestrict=Trueraccomandata quando le sequenze dovrebbero avere uguale lunghezza, consultata il 2026-11-12 - PEP 20: The Zen of Python - l'aforisma «Errors should never pass silently», consultata il 2026-11-12
I listati Python sono stati eseguiti due volte, di seguito, con Python 3.14.4: le uscite riportate coincidono. Il blocco bash con venv e pip install non è stato eseguito (richiede rete e un ambiente da creare): è uno schema da adattare. L'avviso su x is 1 è stato verificato eseguendo l'espressione con python3 -W error.
Commenti
Caricamento commenti...
Accedi per commentare