Python per i laboratori
I laboratori del corso usano Python e il pacchetto Python-MIP. Del linguaggio serve però poco: saper scrivere un ciclo, costruire una lista o un dizionario con una comprensione, definire una funzione. Tutto ciò che compare nei modelli dei capitoli seguenti si riduce a queste poche costruzioni, e questo capitolo le raccoglie in un unico posto. Chi conosce già Python può saltare direttamente alla sezione sui dizionari, che è la struttura dati su cui si appoggiano quasi tutti i modelli, e al troubleshooting finale.
I blocchi di codice contrassegnati dal pulsante Run si eseguono direttamente nel browser: conviene modificarli e rilanciarli. Il codice che usa Python-MIP non è eseguibile nella pagina (il solver è un programma nativo) e compare come listato da leggere.
1. Notebook e Google Colab#
Un notebook (file .ipynb) è una sequenza di celle. Una cella contiene testo formattato in Markdown (con formule come ) oppure codice Python. Una cella di codice si esegue con il pulsante Run o con Shift+Enter; il risultato compare subito sotto e resta lì finché non si rilancia la cella.
Nei laboratori i notebook si aprono su Google Colab: basta un account Google, si carica il file .ipynb e si lavora nel browser senza installare nulla. Ogni notebook dei lab comincia con due celle fisse, l’installazione del pacchetto e la sua importazione:
!pip install mip # il punto esclamativo esegue un comando di shell dentro Colab
import mipIl tempo di esecuzione compare a fianco di ogni cella; per le prime celle è zero perché non c’è nulla da calcolare.
Python vede le celle come un’unica sequenza di istruzioni. Tutto ciò che una cella crea (variabili, funzioni, vincoli di un modello) resta in memoria finché non si riavvia il kernel con Runtime / Restart. Due conseguenze pratiche:
- se si modifica una cella bisogna rilanciarla, e poi rilanciare anche le celle che dipendono da essa;
- una cella eseguita due volte fa il suo lavoro due volte: un
l.append(3)lanciato quattro volte aggiunge quattro3alla lista.
Il riavvio cancella la memoria e gli output, non il testo delle celle. Un ciclo che non termina si interrompe con il quadrato nero in alto.
2. Primi passi: variabili e stampa#
Una variabile si crea assegnandole un valore con =. Non c’è dichiarazione di tipo e non c’è punto e virgola a fine riga.
La seconda print usa una f-string: la f prima delle virgolette abilita la sostituzione delle espressioni fra parentesi graffe. Le f-string compaiono ovunque nei notebook per stampare soluzioni con un formato controllato, per esempio {obj:10.2f} stampa un numero su 10 caratteri con 2 decimali.
Gli operatori aritmetici sono quelli usuali, con tre aggiunte da ricordare: ** è la potenza (3**4 vale 81), // è la divisione intera e % è il resto della divisione (17 % 6 vale 5).
3. Controllo di flusso e funzioni#
L’esempio guida del Lab 0 è un test di primalità: è primo se nessun intero fra 2 e lo divide. Non serve controllare divisori più grandi di , perché se con allora è già stato controllato.
Algoritmo PrimeNumber
Input: n Output: 1 se n è primo, 0 altrimenti
k = 2
finché k <= sqrt(n):
se n è divisibile per k: restituisci 0
incrementa k
restituisci 1
La traduzione in Python incapsula l’algoritmo in una funzione, introdotta da def, che restituisce True o False:
Tre osservazioni, tutte fonti tipiche di errore.
rangeesclude l’estremo destro.range(10)produce i numeri da 0 a 9,range(3, 10)da 3 a 9,range(3, 100, 7)da 3 a 100 a passi di 7. Per arrivare fino a incluso serve quindi il+ 1.L’indentazione è la sintassi. Non ci sono parentesi graffe né
end: le righe consecutive con lo stesso rientro formano un blocco. I due frammenti seguenti fanno cose diverse: il primo stampa0 42 1 42 2 42, il secondo0 1 2 42.for i in range(3): print(i) print(42) for i in range(3): print(i) print(42)import mathserve per le funzioni non elementari (sqrt,floor); le quattro operazioni,//,%e**sono già nel linguaggio.
La prima versione dello stesso programma, nel notebook introduttivo, non usa il resto ma confronta divisione intera e divisione reale (n // k == n / k) e salva i divisori trovati in una lista, per poi stamparli tutti, compresi quelli maggiori di ottenuti come n // d:
- Stampare tutti i numeri primi da 2 a 1000 usando
PrimeNumber. - Scrivere una funzione che calcoli i primi 30 numeri di Fibonacci, con e .
Soluzione
Per il primo basta un ciclo con un if; per il secondo si costruisce la lista aggiungendo ogni volta la somma degli ultimi due elementi (fib[-1] e fib[-2] sono l’ultimo e il penultimo).
4. Liste#
Una lista è una sequenza ordinata e modificabile di oggetti, anche di tipo diverso: l = [1, 2, "hey!"]. Le operazioni che ricorrono nei lab:
l.append(5)aggiunge un elemento in coda;l.extend([3, 4])concatena un’altra lista;l1 + l2produce la lista concatenata;- gli indici partono da zero:
l[0]è il primo elemento; gli indici negativi contano dalla fine,l[-1]è l’ultimo; len(l)è la lunghezza,max(l)esum(l)il massimo e la somma;x in lvaleTruesexcompare nella lista.
5. List comprehension#
In matematica un insieme si definisce spesso per comprensione, per esempio . Python permette la stessa scrittura in una riga:
Il risultato è la lista [108, 256, 500, 864, 1372, 2048, 2916, 4000]. Attenzione: S è una lista, non un insieme, quindi ogni elemento ha una posizione, e la posizione non ha relazione con il valore di i che l’ha generato (S[3] è 864, prodotto da ).
La comprensione accetta anche una condizione: da si estrae l’insieme dei primi con
S1 = range(2, 101)
S2 = [i for i in S1 if PrimeNumber(i)]
print(S2)(il notebook originale stampa P invece di S2, un refuso: la variabile definita si chiama S2).
Una list comprehension si legge da destra a sinistra come una frase: «per ogni i in range(3, 11), prendi 4*i**3». Se c’è un if, si applica prima il filtro e poi si trasforma. È esattamente il modo in cui nei modelli si scrivono le somme e i vincoli «per ogni »: la comprensione è il ciclo for scritto dentro le parentesi.
Un piccolo esercizio del notebook chiede di calcolare : gli interi Python non hanno limite di dimensione, quindi 2**1000 si stampa senza problemi, un numero di 302 cifre.
6. Dizionari e tuple#
6.1 Dizionari#
Un dizionario associa a ogni chiave un valore. È la struttura più usata nei laboratori, perché le variabili e i vincoli di un modello sono indicizzati da insiemi che spesso non sono numeri consecutivi (coppie di nodi, nomi di prodotti).
Un dizionario vuoto è {}; d.keys(), d.values() e d.items() restituiscono rispettivamente le chiavi, i valori e le coppie, e permettono di iterare su tutto il contenuto. Come le liste, anche i dizionari si costruiscono per comprensione: {i: len(i) for i in names} associa a ogni nome la sua lunghezza.
L’esempio del notebook sulle popolazioni combina comprensione e filtro sulle chiavi:
Stampare la popolazione media dei paesi il cui nome termina per y, poi la popolazione massima fra i paesi con nome di 7 lettere. Servono solo len(s), s[-1] (ultimo carattere di una stringa) e max(l).
Soluzione
I paesi che finiscono per y sono Germany, Italy e Norway, con media 49272.3 migliaia; quelli con sette lettere sono Austria, Belgium, Denmark, Finland e Germany, e il massimo è la Germania con 83191.
6.2 Tuple#
Una tupla è una sequenza immutabile: t = (a1, a2, a3). Si accede agli elementi come in una lista (t[0]), ma non si può modificare un elemento dopo la creazione: t[2] = 8 produce un errore, mentre su una lista funziona.
Il motivo per cui le tuple contano nei lab è che possono fare da chiave di un dizionario, le liste no. Un dizionario come {(1,2): 12, (3,6): 2} associa coppie a numeri: è esattamente la forma delle variabili indicizzate su archi di un grafo nel capitolo 2. L’esempio del notebook costruisce, per coppie , le coordinate polari e :
La doppia for dentro la comprensione (for i in S1 for j in S2) enumera tutte le coppie: è il prodotto cartesiano , la stessa costruzione con cui si genera l’insieme degli archi di un grafo.
- Completare la funzione
prime_factorsche, dato un numero, restituisce la lista dei suoi fattori primi. - Costruire un dizionario che associa a ogni numero da 2 a 100 la lista dei suoi fattori primi (a 57 deve corrispondere
[3, 19]). - Costruire un dizionario che associa a ogni da 2 a 50 il numero di interi positivi sotto 100 di cui è fattore primo (a 7 deve corrispondere 14: i multipli 7, 14, …, 98).
def prime_factors(n):
factors = []
decomp = n
i = 2
while i <= decomp and i <= n:
while decomp % i == 0:
decomp = decomp / i
if i not in factors:
# TODO: aggiungere i a factors
i += 1
return factorsSoluzione
L’istruzione mancante è factors.append(i). I due dizionari si costruiscono per comprensione; per il terzo si contano, per ogni , i numeri la cui lista di fattori contiene .
Osserviamo che conteggio[k] è zero per ogni non primo, perché un numero composto non compare mai come fattore primo.
7. NumPy e Matplotlib#
Due moduli esterni compaiono nei lab sui grafi. NumPy offre i multiarray (vettori, matrici) con operazioni elemento per elemento e prodotto matriciale con @; Matplotlib disegna i grafici. Si importano con un alias per brevità.
Con np.random.random((k, 2)) si generano coppie di numeri casuali in ; moltiplicando per 100 si ottengono punti in un quadrato di lato 100, la costruzione con cui il Lab 2 crea le istanze. Un grafico a dispersione dei punti si ottiene con plt.scatter(point[:,0], point[:,1]) seguito da plt.show(), dove point[:,0] seleziona la prima colonna.
8. Errori frequenti e troubleshooting#
La sezione conclusiva del primo notebook del Lab 1 raccoglie le cause più comuni di errore. Vale la pena conoscerle prima di scrivere il primo modello.
Indentazione. In un for, in un if o in una def il corpo deve essere rientrato in modo coerente. Danno errore sia un corpo non rientrato, sia due righe con rientro diverso nello stesso blocco:
for i in [1,2,3]:
print(i) # errore: manca il rientro
if i==4:
print('i is 4')
print('deal with it') # errore: rientro incoerenteIl rientro consigliato è di 4 spazi.
Assegnamento e uguaglianza. = assegna, == confronta. if a = 4 è un errore di sintassi; a == 4 da solo è lecito ma non fa nulla (restituisce True o False). Questo distinguo torna nei vincoli dei modelli: un vincolo di uguaglianza si scrive con ==, mai con =.
Istruzioni su più righe. Una condizione lunga si spezza aggiungendo \ a fine riga, oppure lasciando aperta una parentesi: dentro parentesi non chiuse il ritorno a capo è libero. È il caso più frequente nei modelli, dove m.add_constr(...) occupa spesso due o tre righe.
if (i==3 or i==5 or
i==7):
print('i is prime')Persistenza delle celle. È l’errore più insidioso perché non produce alcun messaggio. Il notebook lo illustra con un modello Python-MIP: si risolve con il vincolo e si ottiene . Poi, per «rilassare» il problema, si aggiunge in una nuova cella il vincolo e si riottimizza:
m.add_constr(2*x1 + x2 <= 20)
m.optimize()
print('solution:', x1.x, ',', x2.x) # ancora 0.0 , 10.0La soluzione non cambia. Il modello ora ha due vincoli, quello vecchio e quello nuovo, e il più restrittivo continua a valere: add_constr aggiunge, non sostituisce. Per modificare un vincolo si torna nella cella che lo definisce, la si modifica e si rilanciano tutte le celle da lì in avanti; nel dubbio si riavvia il kernel e si esegue tutto dall’inizio.
Stile. Chi vuole verificare che il proprio codice segua lo standard può eseguire flake8 myprogram.py, che segnala ogni deviazione (di solito molte).
9. Riepilogo#
| Costruzione | Sintassi | Dove serve nei lab |
|---|---|---|
| Lista per comprensione | [expr for i in S if cond] |
vettori di variabili [m.add_var() for i in range(n)] |
| Dizionario per comprensione | {k: expr for k in S} |
variabili su archi {(i,j): m.add_var() for (i,j) in A} |
Doppio for |
for i in V for j in V |
insiemi di archi e coppie |
| Somma di un generatore | sum(expr for i in S) |
in Python-MIP diventa mip.xsum(...) |
| Tupla come chiave | x[i,j] |
accesso alle variabili indicizzate su coppie |
range(n) |
da 0 a | indici delle variabili, sempre da zero |
| f-string | f"{valore:8.2f}" |
stampa formattata delle soluzioni |