Salta al contenuto principale

08 - Python per i dati 1: ambiente e fondamenti

Ambiente virtuale riproducibile e fondamenti di Python per i dati: nomi e oggetti, divisione intera con i negativi, condizioni e cicli, con listati eseguiti.

Cosa ne pensi di questo articolo?

08 - Python per i dati 1: ambiente e fondamenti
Machine Learning Engineerfedericocalo.dev

In Breve

Ambiente virtuale riproducibile e fondamenti di Python per i dati: nomi e oggetti, divisione intera con i negativi, condizioni e cicli, con listati eseguiti.

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:

  1. python -m pip, non pip: garantisce che il pacchetto vada nell'interprete che stai usando.
  2. Un ambiente per progetto, cartella .venv esclusa dal controllo di versione.
  3. requirements.txt (o equivalente) versionato: chi arriva dopo di te ricrea lo stesso ambiente con python -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.

Nomi e oggettiA sinistra due nomi puntano allo stesso oggetto; a destra due oggetti diversi con lo stesso contenuto.a = [1, 2]; b = aa = [1, 2]; b = [1, 2]ab[1, 2]un solo oggetto, due nomib.append(3) cambia anche aab[1, 2][1, 2]due oggetti uguali ma distinti
Figura: un nome è un'etichetta, non una scatola.

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.

  • is confronta 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.

  1. Quanto vale len(letture) dopo le due righe?
  2. Che cosa restituiscono letture is copia e letture == copia?
  3. 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:

  1. −7 / 3 = −2.333…, il pavimento è −3. Quindi −7 // 3 = −3.
  2. 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.

  1. Quanto valgono -150 // 60 e -150 % 60? Verifica che l'identità a == (a // b) * b + (a % b) regga.
  2. Che ora è, scritta come HH:MM? (Suggerimento: riporta prima il minuto dentro la giornata con % 1440.)
  3. 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 for può 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, non x2).
  • Un ambiente per progetto; requirements.txt versionato.
  • 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.

  1. Valida ogni voto con classifica_voto-like logic; scarta (e conta) quelli fuori da 0-30 intercettando ValueError.
  2. Calcola la media come somma / quanti, gestendo il caso lista vuota (che cosa deve restituire?).
  3. Aggiungi tre assert come 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 for e range.
  • Medium. Scrivi e_primo(n) con un ciclo fino a int(n**0.5). Perché basta arrivare alla radice quadrata? (Se n = a·b con a ≤ b, allora a ≤ √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 == a per 1000 coppie casuali con segni misti. Poi spiega perché int(a / b) non è equivalente a a // b per i negativi.
  • Enterprise. Prepara per un team un template di progetto: struttura cartelle, .gitignore per .venv, requirements.txt, una pagina README con 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

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

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti