Python: un'introduzione¶
In questo notebook faremo una rapida introduzione a Python tramite con esempi la cui comprensione è essenziale per il corso.
Le risorse per approfondire sono riportate in fondo: vanno oltre quanto richiesto per l'esame, ma sono utili per continuare a studiare in autonomia. Inoltre, come sempre, una maggiore conoscenza di alcuni aspetti facilita l'apprendimento degli altri.
Obiettivi formativi¶
Al termine di questo notebook dovresti essere in grado di:
- Riconoscere i tipi di dato e le strutture di controllo base di Python.
- Scrivere piccoli script con variabili, condizioni, cicli e liste.
- Usare slicing e cicli su stringhe/liste in modo consapevole.
- Capire gli errori più comuni legati a mutabilità e assegnamento per riferimento.
Caratteristiche salienti del linguaggio¶
Python è un linguaggio di programmazione a oggetti, interpretato, dinamico e di alto livello.
Imperativo a oggetti (OOP: Object Oriented Programming)¶
Python supporta diversi paradigmi di programmazione. I principali sono quello imperativo a oggetti e quello funzionale; in questo corso ci concentreremo soprattutto sul primo. In Python è frequente creare nuovi tipi di dati tramite classi, e molte classi utili sono già fornite dal linguaggio (liste, dizionari, stringhe, ecc.).
Essendo un linguaggio imperativo, le istruzioni vengono eseguite nell'ordine in cui sono lanciate dall'utente. Nei Jupyter notebook questo ordine è quello dei numeri a sinistra delle celle, non necessariamente l'ordine visivo delle celle stesse. All'interno della stessa cella le istruzioni vengono eseguite dall'alto verso il basso seguendo il numero di riga visibile a sinistra (per attivarlo cliccate su "View" -> "Toggle Line Numbers").
Interpretato¶
I comandi Python sono eseguiti da un processo chiamato interprete. In alcuni casi, questo processo è rappresentato dalla finestra del terminale, motivo per cui chiudere il terminale chiude anche l'interprete e blocca l'esecuzione dei comandi Python.
L'interprete permette di scrivere un'istruzione e verificarne subito il risultato. In classe abbiamo usato Jupyter notebook, che fornisce un'interfaccia comoda sopra al kernel Python. Potete vederlo come un interfaccia grafica che parla con l'interprete Python sottostante e mostra i risultati in modo più leggibile.
Naturalmente, è possibile scrivere dei programmi anche molto complessi in Python, e lanciarli in modo non interattivo. I comandi al loro interno sono tuttavia sempre eseguiti dall'interprete. Python in particolare permette di salvare su file di testo sia
- script o programmi (gli script sono in genere programmi molto brevi che automatizzano dei compiti svolti dall'utente), file contenenti codice eseguibile in autonomia ed in modo non interattivo;
- moduli: file contenenti funzioni e classi importabili da altri script Python come ad esempio funzioni matematiche particolari.
Il vantaggio dei moduli è che permettono di organizzare, condividere e testare il codice in modo più efficiente. Una volta correttamente testato un moduli, siamo sicuri della sua correttezza e possiamo importarlo in più script Python senza doverlo riscrivere ogni volta, evitando di dover riscrivere funzioni e classi comuni in ogni script.
Dinamico¶
In Python non è necessario dichiarare esplicitamente il tipo di una variabile al momento della creazione: l'interprete lo deduce automaticamente e ne tiene traccia durante l'esecuzione del programma.
a = 10 # int
b = 0.5 # float
Questo rende la scrittura del codice rapida e flessibile, ma può anche introdurre errori più sottili se non si controllano bene i tipi durante lo sviluppo.
Se avete dubbi relativi al tipo di una variabile (o di qualsiasi altra cosa), potete usare la funzione type() per verificarlo:
a = 10
print(f"a = {a}, type(a) = {type(a)}")
b = a / 2
print(f"b = {b}, type(b) = {type(b)}")
print(f"type(0.0)={type(0.0)}")
print(f"type('hello')={type('hello')}")
print(f"type(print)={type(print)}")
a = 10, type(a) = <class 'int'>
b = 5.0, type(b) = <class 'float'>
type(0.0)=<class 'float'>
type('hello')=<class 'str'>
type(print)=<class 'builtin_function_or_method'>
Di alto livello¶
Scrivere in Python è quasi come scrivere pseudocodice in inglese. Questo avviene perché il linguaggio mette a disposizione tutta una serie di oggetti (ad esempio liste e stringhe), keyword e funzioni che non sono presenti in altri linguaggi.
Questo permette di scrivere codici come il seguente:
# Inizializzo una lista `mylist` con 4 elementi di tipi diversi: 1, 2, 'a' e 'ciao'.
mylist = [1,2, 'a', 'ciao']
# Se l'elemento 'a' è presente in `mylist`, allora stampo "ehi!!"
if 'a' in mylist: # "in" è un operatore logico di Python
print("ehi!!")
ehi!!
Notate che se dovessimo ragionare come abbiamo visto nelle lezioni precedenti, dovremmo scorrere tutti gli elementi di mylist uno ad uno, e valutare se qualcuno di questi è uguale ad 'a', se sì, stampare "ehi" ed uscire:
mylist = [1,2, 'a', 'ciao']
n = len(mylist) # lunghezza della lista
i = 0 # contatore
while(i<n):
if mylist[i] == 'a': # il codice relativo all' if è identificato dall'indentazione
print("ehi!")
break # interrompe il ciclo (lo vedremo poi)
i = i + 1 # se ci dimentichiamo questo il ciclo continua all'infinito!
ehi!
Esercizio - Ricerca di un elemento in lista¶
Scrivi una lista che contenga almeno 5 elementi misti (numeri e stringhe). Poi verifica con
if ... in ...se una stringa scelta da te è presente nella lista.
Vediamo alcuni aspetti base del linguaggio¶
Variabili e tipi¶
Istruzioni come a = b + c assegnano alla variabile a sinistra il risultato dell'espressione a destra. È lo stesso concetto dell'assegnamento $a \leftarrow b + c$ visto nelle prime lezioni.
Per esempio:
$$ k = 10 $$ $$ k \leftarrow k - 1 $$
in Python diventano:
k = 10
k = k - 1
Tipi di dati numerici¶
Python supporta nativamente numeri in virgola mobile (float), interi (int) e booleani (True, False). Il tipo viene stabilito automaticamente dall'interprete. (ricordate che il potete verificare il tipo di una variabile tramite la funzione type()).
Nota: vedrete spesso comparire il termine class, perché in Python ogni oggetto appartiene a una classe. Per il livello attuale potete pensare ad una classe come ad un "tipo di dato". Per esempio, la classe int rappresenta il tipo di dato intero, la classe str rappresenta il tipo di dato stringa, e così via.
a = 2
b = 2.5
c = True
print(type(a))
print(type(b))
print(type(c))
<class 'int'> <class 'float'> <class 'bool'>
Nomi validi per le variabili¶
Uno può trovarli per prova ed errore, vedendo quando l'interprete si lamenta... Comunque, le regole sono le seguenti.
- I nomi possono contenere lettere, numeri, ed il carattere
_(underscore o trattino basso) - NON possono iniziare con un numero:
0_xnon è una variabile valida.x_0lo è - SONO "case sensitive":
Ciccio != ciccio != cIccio
Esercizio - Nomi di variabili validi¶
Dato il seguente codice, quali sono le variabili valide? Quale no? Perché?
x_0 = 1 _nome = 2 # 0x = 3 # non validoProva ad eseguire il codice dato e controlla quale errore ti restituisce l'interprete.
Operazioni aritmetiche¶
Le operazioni base supportate sono +, -, *, /, // (div. intera), ** (potenza), %(resto).
Operazioni più complesse quali ad esempio logaritmi, funzioni trigonometriche, ecc. sono disponibili tramite moduli esterni come math e cmath (per i numeri complessi), ma questo è un argomento che vedremo più avanti.
a = 3
b = 2
# print è una funzione che stampa a schermo uno o più argomenti, descritta più sotto.
print(f"a = {a}, b = {b}")
print(f"a+b = {a+b}, a-b = {a-b}")
print(f"a*b = {a*b}")
print(f"a/b = {a/b} (risultato a virgola mobile)")
print(f"a//b = {a//b} (risultato negli interi)")
print(f"a**b = {a**b}")
print(f"a%b = {a%b} (modulo)")
a = 3, b = 2 a+b = 5, a-b = 1 a*b = 6 a/b = 1.5 (risultato a virgola mobile) a//b = 1 (risultato negli interi) a**b = 9 a%b = 1 (modulo)
Esercizio - Operazioni aritmetiche base¶
Imposta due variabili intere
aeba tua scelta e stampa: somma, differenza, prodotto, divisione reale, divisione intera e resto. Verifica che i risultati siano coerenti e che il tipo dei risultati sia quello atteso.
Precedenza delle operazioni¶
Queste operazioni hanno un'ordine di precedenza (precedenza degli operatori):
**, *, //, /, +, -, %
quindi ** viene eseguito prima di * etc. etc. Per cambiarlo, usiamo le parentesi.
print(2**2*3)
12
print(2**(2*3))
64
Controllo del flusso¶
Scelte: if - elif - else¶
Per effettuare scelte binarie, usiamo il costrutto
if cond1:
istruzione
...
elif cond2:
istruzione
...
...
else:
istruzione
Nota, indentazione: l'indentazione è fondamentale in quanto permette di definire i blocchi di codice associati a ciascuna condizione.
Nota, due punti: i due punti : sono necessari per indicare l'inizio del blocco di codice associato alla condizione. Senza i due punti, l'interprete non saprebbe dove inizia il blocco di codice da eseguire quando la condizione è vera.
Nota, else ed elif: le keyword else ed elif sono opzionali. Se non sono presenti, il programma continua ad eseguire le istruzioni successive al blocco if anche se nessuna delle condizioni è vera. Se invece sono presenti, il programma esegue solo le istruzioni del blocco associato alla prima condizione vera e salta tutte le altre condizioni e i loro blocchi di codice associati. È possibile avere più di un blocco elif per gestire più condizioni, ma solo un blocco else alla fine per gestire il caso in cui nessuna delle condizioni precedenti sia vera.
a = 9
b = 20
c = 5
if (a>b):
print(f"Ciao, {a=} è maggiore di {b=}")
elif (a<c):
print(f"Addio, {a=} è minore di {c=}")
else:
print(f"Fen finta de pomi") # "facciamo finta di niente
if(b>c):
print(f"Mamma mia! {b=} è maggiore di {c=}")
if(a==b):
print(f"Wow, {a=} è uguale a {b=}")
else:
print(f"Peccato, {a=} è diverso da {b=}")
Fen finta de pomi Mamma mia! b=20 è maggiore di c=5 Peccato, a=9 è diverso da b=20
Nota, valore di una variabile: usando una f-string è possibile stampare sia il nome che il valore di una variabile aggiungendo il simbolo di uguaglianza = dopo il nome della variabile all'interno delle parentesi graffe.
Il ciclo while¶
Il ciclo while si usa per ripetere un insieme di istruzioni fino a quando una data condizione è verificata:
while(_condizione è vera_):
...
k = 10
while(k>0):
k = k - 1
print(k)
# notare che lo zero viene raggiunto!
print(k)
9 8 7 6 5 4 3 2 1 0 0
All'interno di un ciclo while o di un if non è obbligatorio inserire un confronto, è possibile anche utilizzare un valore booleano direttamente.
Ad esempio, se non avessimo la parola chiave in potremmo scrivere un ciclo per cercare un elemento in una lista come il seguente:
lista_di_numeri = [1, 2, 3, 4, 5]
trovato = False
indice = 0
while not trovato:
if lista_di_numeri[indice] == 3:
trovato = True
indice += 1
Esercizio - While con numeri pari¶
Modifica il ciclo
whilein modo che stampi solo i valori pari da 10 a 0.
Esempio: calcolare la somma dei numeri $1..N$¶
Per scrivere algoritmi iterativi in un linguaggio imperativo dobbiamo saper aggiornare variabili e contatori. Questo può eservirci, ad esempio, per tener traccia di quante volte abbiamo eseguito le operazioni all'interno di un ciclo o per aggiornare il risultato di un calcolo iterativo.
Supponiamo, ad esempio, di voler calcolare quanto fa la somma dei numeri da $1$ an $N$ incluso. Com'è noto, il risultato di questa operazione elementare[1] è $$S_n = \frac{n(n+1)}{2}$$
Per rendersi conto che $\sum_{k=1}^n k= \frac{n(n+1)}{2}$ basta considerare la somma $2\sum_{k=1}^{n}k$ e sommare in colonna come riportato qui sotto.
1 + 2 + 3 + ... + n-2 + n-1 + n +
n + n-1 + n-2 + ... + 3 + 2 + 1 =
----------------------------------------
n+1 + n+1 + n+1 + .... + n+1 + n+1 + n+1 = n*(n+1)
[1] Elementare soprattutto se siete Gauss.
Se volessimo invece usare un computer per eseguire il calcolo esplicito, avremmo invece bisogno di due variabili che vengono incrementate durante il ciclo. Una, ovviamente, è il risultato, chiamiamolo s (da sommatoria); e l'altra è k (l
indice standard nelle sommatorie), cioè il contatore che tiene conto di quante somme abbiamo effettuato fino a quel momento, e che in questo caso coincide con il valore da sommare a s. In Python:
n = 100
k = 1
s = 0 # variabile su cui salviamo il risultato
while(k<=n):
# Aggiorniamo il valore della somma
# aggiungendo k al valore precedente di s
s = s + k
# Aggiorniamo il valore di k incrementandolo di 1
k = k + 1
Addizione-ed-assegnamento: sono molto frequenti i casi in cui vogliamo aggiornare il valore di una variabile facendo un operazione su di essa. Per semplificarci la vita, Python mette a disposizione delle forme abbreviate per l'addizione +=, la sottrazione -=, la moltiplicazione *= e la divisione /=, che permettono di aggiornare il valore di una variabile dopo aver eseguito l'operazione corrispondente. Ad esempio, se vogliamo aggiungere ad s il valore di k, invece di scrivere s = s + k, possiamo semplicemente scrivere s += k. Allo stesso modo, se vogliamo incrementare k di 1, invece di scrivere k = k + 1, possiamo scrivere k += 1. Queste forme abbreviate rendono il codice più conciso e leggibile, soprattutto quando si tratta di operazioni comuni come l'aggiornamento di contatori o somme.
Nota: l'operazione-e-assegnamento ha priorità minima. Verranno prima svolte tutte le operazioni a destra dell'operatore-e-assegnamento, e solo alla fine verrà aggiornato il valore della variabile a sinistra. Ad esempio, se scriviamo
a = 5
m = 10
m *= a + 1
la riga m *= a + 1 verrà interpretata come m = m * (a + 1), quindi prima viene calcolato a + 1 e poi il risultato viene moltiplicato per m.
n = 100
k = 1
s = 0 # variabile su cui salviamo il risultato
while(k<=n):
s += k # equivalente a s = s + k
k += 1 # equivalente a k = k + 1
s_con_formula = n*(n+1)//2
print(f"Il risultato con il ciclo è {s}")
print(f"Quello con la formula è {s_con_formula}.")
print(f"La differenza è {s - s_con_formula}")
Il risultato con il ciclo è 5050 Quello con la formula è 5050. La differenza è 0
Esercizio - Fattoriale con while¶
Scrivi un ciclo
whileche calcoli il fattoriale di un numerona tua scelta usando l'operatore di moltiplicazione e assegnamento*=. Ricorda che il fattoriale di un numeronè definito come il prodotto di tutti i numeri interi da 1 anincluso, ed è denotato comen!. Ad esempio,5! = 5 * 4 * 3 * 2 * 1 = 120. Supponendo di avere un intero a 32 bit, senza segno, fino a che numero è possibile calcolare il fattoriale prima di andare in overflow?
Il ciclo for¶
Il ciclo for si usa per ripetere una sequenza di istruzioni un numero fissato di volte, quando sappiamo già qual è questo numero. Rispetto a while ci risparmia di incrementare (o decrementare) il contatore esplicitamente, perché se ne occupa l'interprete.
Chi ha studiato C o altri linguaggi simili è probabilmente abituato a scrivere i cicli for in questo modo:
for(condizione inizio; condizione fine; incremento) { ... }
Ad esempio la sommatoria di prima diventa:
int s=0;
for(int k = 1; k<= 100; k+=1) {
s+=k;
}
In Python, condizione iniziale, finale e incremento si specificano con la funzione range(start,end,increment)=range(s,e,i). Questa restituisce ad ogni invocazione il numero successivo in una serie numerica $\{s,s+i, s+2i,\dots, s+m\,i\}$ il cui ultimo elemento è tale che $s+m\,i < e$. In altre parole, l'elemento end non viene mai raggiunto.
Ricapitalando: range(start, end, increment) restituisce una sequenza di numeri interi che
- parte da
startincluso, (default: 0) - arriva fino a
endescluso (parametro obbligatorio), - fa passi di lunghezza da
increment(default: 1, può essere negativo).
Se un parametro non viene specificato, python usa il suo valore di default indicato sopra.
Vediamo alcuni esempi:
for i in range(3): # end=3, sottintende che start=0 e incremento=1
print(i)
0 1 2
for j in range(1,5): # start=1, end=5, sottintende che incremento=1
print(j)
1 2 3 4
for k in range(1,5,2): # start=1, end=5, incremento=2
print(k)
1 3
for t in range(7,0,-2): # start=7, end=0, incremento=-2 (decremento)
print(t)
7 5 3 1
Attenzione: start, end, e increment devono essere numeri interi. Se uno di questi è un numero in virgola mobile, Python restituirà un errore.
Esercizio - Uso di range nel for¶
Scrivi tre cicli
forconrangeche producano:
- I numeri da 0 a 9,
- I numeri da 10 a 0 con passo -1,
- I multipli di 3 minori di 30 (30 escluso).
- I multipli di 4 minori di 40 (40 incluso).
- I numeri da 0 a 10 con passo 0.5 (questo è un caso in cui
rangenon funziona e bisogna compiere delle operazioni sul valore dell'indice).
La nostra sommatoria precedente, scritta con il ciclo for diventa:
s = 0
n = 100
for k in range(n+1): #attenzione: l'ultimo valore è sempre escluso!!
s += k
print(s)
5050
Esercizio - Fattoriale con for¶
Riscrivi il calcolo del fattoriale $N!$ usando un ciclo
for.
Esecizio - Doppio fattoriale¶
Calcola il doppio fattoriale $N!!$ di un numero
Na tua scelta usando un ciclofor. Il doppio fattoriale di un numeroNè definito come il prodotto di tutti i numeri interi da 1 aNa salti di due.
Ad esempio,8!! = 8 * 6 * 4 * 2 = 384, mentre7!! = 7 * 5 * 3 * 1 = 105.
Liste: introduzione rapida¶
Ora che sappiamo usare i cicli, ci serve una struttura dati su cui applicarli, ad esempio un insieme di valori
$$ X = \{X_1, X_2, X_3, \ldots, X_n\}. $$
In Python esistono varie strutture dati per rappresentare insiemi di valori, ma quella più semplice e versatile è la lista.
l = [1, 2, 4, 6, 7]
Le liste in Python hanno le seguenti caratteristiche:
- Al contrario di Matlab ma similmente a
C, sono indicizzate a partire da 0, non da 1. - È possibile accedere a singoli elementi tramite il loro indice. Indice negativi corrispondono ad elementi a partire dalla fine della lista (ad esempio,
l[-1]restituisce l'ultimo elemento). - Sono mutabili, il che significa che è possibile modificare i loro elementi dopo la creazione
- Possono contenere elementi di tipi diversi (ad esempio, numeri, stringhe, altre liste, ecc.)
- Possono essere estese dinamicamente, cioè è possibile aggiungere o rimuovere elementi in qualsiasi momento tramite apposite funzioni
- Mantengono l'ordine di inserimento degli elementi, il che significa che gli elementi vengono restituiti nell'ordine in cui sono stati aggiunti alla lista.
Nel caso si cercasse di accedere a un indice che non esiste, Python restituirebbe un errore IndexError indicando che l'indice è fuori dal range della lista.
Puoi sempre verificare la lunghezza di una lista con la funzione len(), che restituisce il numero di elementi presenti nella lista.
Facciamo alcuni esempi:
l = [0,2,4,6,7, -3, 2, 5] # Genero una lista di numeri
print(f"La lista {l=} è di tipo {type(l)}. L'ordine degli elementi è quello in cui li ho inseriti.\n")
# Le liste sono indicizzate a partire da 0 e posso accedere ai singoli elementi tramite il loro indice.
print(f"Il primo elemento di l è {l[0]=}, il secondo è {l[1]=}, il terzo è {l[2]=}.\n")
# Posso anche accedere agli elementi a partire dalla fine della lista, usando indici negativi. L'ultimo elemento è l[7] o, equivalentemente, l[-1].
print(f"L'ultimo elemento è {l[7]=} o, equivalentemente, {l[-1]=}.\n ")
# Le liste sono mutabili, cioè posso modificare i loro elementi.
l[0] = 10
print(f"Dopo aver modificato il primo elemento, la lista è {l=} ed il primo elemento è {l[0]=}.\n")
La lista l=[0, 2, 4, 6, 7, -3, 2, 5] è di tipo <class 'list'>. L'ordine degli elementi è quello in cui li ho inseriti. Il primo elemento di l è l[0]=0, il secondo è l[1]=2, il terzo è l[2]=4. L'ultimo elemento è l[7]=5 o, equivalentemente, l[-1]=5. Dopo aver modificato il primo elemento, la lista è l=[10, 2, 4, 6, 7, -3, 2, 5] ed il primo elemento è l[0]=10.
# Le liste possono contenere elementi di tipi diversi
zibaldone = [1, 2.5, 'ciao', [1,2,3], True, print, l]
lunghezza_zibaldone = len(zibaldone)
print(f"La lista {zibaldone=} contiene {lunghezza_zibaldone} elementi di tipi diversi:")
for i in range(lunghezza_zibaldone):
print(f"{i}) {zibaldone[i]} è di tipo {type(zibaldone[i])}\n")
La lista zibaldone=[1, 2.5, 'ciao', [1, 2, 3], True, <built-in function print>, [10, 2, 4, 6, 7, -3, 2, 5]] contiene 7 elementi di tipi diversi: 0) 1 è di tipo <class 'int'> 1) 2.5 è di tipo <class 'float'> 2) ciao è di tipo <class 'str'> 3) [1, 2, 3] è di tipo <class 'list'> 4) True è di tipo <class 'bool'> 5) <built-in function print> è di tipo <class 'builtin_function_or_method'> 6) [10, 2, 4, 6, 7, -3, 2, 5] è di tipo <class 'list'>
lista_vuota = []
print(f"La lista {lista_vuota=} è di tipo {type(lista_vuota)} e contiene {len(lista_vuota)} elementi.\n")
lista_vuota.append(1)
print(f"Dopo aver aggiunto un elemento, la lista è {lista_vuota=} e contiene {len(lista_vuota)} elementi.\n")
print(f"il che significa che la lista non è più vuota ed il nome che le avevamo dato non è più appropriato!\n")
La lista lista_vuota=[] è di tipo <class 'list'> e contiene 0 elementi. Dopo aver aggiunto un elemento, la lista è lista_vuota=[1] e contiene 1 elementi. il che significa che la lista non è più vuota ed il nome che le avevamo dato non è più appropriato!
scorrere le liste in ordine inverso¶
Spesso è utile accedere agli ultimi elementi di una lista. Questo in Python si può fare facilmente usando indici negativi:
l = [1, 2, 4, 6, 7]
#indici 0 1 2 3 4
# -1 ^
# -2 ^
(AVANZATO) Comprensione di lista: Python fornisce una sintassi speciale per creare nuove liste a partire da altri insiemi sui quali potete fare un ciclo for. Questa sintassi è chiamata "List Comprehension" (comprensione di lista) ed ha una forma particolarmente elegante:
nome_lista = [espressione for elemento in iterabile]
Cioè, l'iterazione che costruisce la lista è indicata dentro le parentesi [ e ] che indicano la funzione che costruisce la lista stessa.
quadrati = [x**2 for x in range(10)]
Questa sintassi è equivalente a scrivere:
quadrati = []
for x in range(10):
quadrati.append(x**2)
e si può leggere come "per ogni elemento x nell'iterabile range(10), calcola x**2 e aggiungilo alla lista quadrati".
PI_GRECO = 3.14 # Più avanti vedremo che esistono costanti predefinite, ma per ora possiamo definirne una noi!
radianti = [ alpha * PI_GRECO / 180 for alpha in range(0, 361, 45) ] # lista di radianti corrispondenti a 0, 30, 60, ..., 360 gradi
radianti
[0.0, 0.785, 1.57, 2.355, 3.14, 3.925, 4.71, 5.495, 6.28]
Slicing¶
In Python (come in matlab) è possibile accedere a parte di una lista usando una notazione nota come slicing. La sintassi base per lo slicing è la seguente:
nomelista[inizio:fine]
Dove:
- inizio è sempre incluso.
- fine è sempre esclusa.
È possibile omettere uno dei due estremi, in questo caso si intende rispettivamente "fino alla fine della lista" o "dall'inizio della lista".
l = [1, 2, 4, 6, 7, 8]
#indici 0 1 2 3 4 5
#slicing 1:4 ^ ^ ^
#slicing 2: ^ ^ ^ ^
#slicing :3 ^ ^ ^
print(f"Lista completa: {l}")
print(f"Slicing da indice 1 a 4 (escluso): {l[1:4]}")
print(f"Slicing da indice 2 fino alla fine: {l[2:]}")
print(f"Slicing dall'inizio della lista fino a indice 3 (escluso): {l[:3]}")
print(f"Slicing completo: {l[:]}") # Equivalente a l
Lista completa: [1, 2, 4, 6, 7, 8] Slicing da indice 1 a 4 (escluso): [2, 4, 6] Slicing da indice 2 fino alla fine: [4, 6, 7, 8] Slicing dall'inizio della lista fino a indice 3 (escluso): [1, 2, 4] Slicing completo: [1, 2, 4, 6, 7, 8]
Infine, è possibile specificare un passo con il quale "scorriamo" la fetta (slice) di lista che vogliamo tenere:
nomelista[inizio:fine:passo]
l = [1, 2, 4, 6, 7]
#indici 0 1 2 3 4
# 1:4:2 ^ ^
l[1:4:2] # slicing con passo 2, da indice 1 a 4 (escluso) prende gli elementi con indice 1 e 3
[2, 6]
Se lasciamo vuoti sia inizio che fine, questo prende gli elementi intervallati da un certo passo.
l = [1, 2, 4, 6, 7]
#indici 0 1 2 3 4
# ::2 ^ ^ ^
l[::2] # slicing completo con passo 2
[1, 4, 7]
Nota come la sintassi dello slicing è molto simile a quella della funzione range(start, stop, step)!
Attenzione: se specificate un valore per stop maggiore della lunghezza della lista, lo slicing si fermerà comunque alla fine della lista senza restituire un errore.
l = [0, 1, 2]
print(f"`l` è lunga {len(l)} ma posso fare uno slice fino a 6: {l[0:6]=}")
`l` è lunga 3 ma posso fare uno slice fino a 6: l[0:6]=[0, 1, 2]
Esercizio - Slicing di liste¶
Data la lista
l = [1, 2, 4, 6, 7, 9, 10], usa slicing per ottenere:
- i primi 4 elementi,
- gli elementi in posizione dispari,
- la lista invertita.
Cicli for sulle liste¶
Python supporta anche i cicli per ogni ("for each") che permettono di iterare direttamente sugli elementi di una lista senza dover usare gli indici. La sintassi è la seguente:
for elemento in lista:
# istruzioni da eseguire per ogni elemento
all'inizio di ogni iterazione, la variabile elemento assume il valore del successivo elemento della lista, e le istruzioni all'interno del ciclo vengono eseguite per quel valore.
Bisogna fare attenzione a due cose:
- Se si modifica la variabile
elementoall'interno del ciclo, questa non modifica l'elemento corrispondente nella lista. La variabileelementoè una copia del valore dell'elemento della lista, è qualsiasi modifica aelementonon influirà sulla lista originale. - Per quanto Python non mostri un errore, è altamente sconsigliato modificare la lista su cui si sta iterando all'interno del ciclo, in quanto questo può portare a comportamenti imprevedibili o errori logici. Se è necessario modificare la lista durante l'iterazione, è meglio creare una copia della lista originale e iterare sulla copia, oppure utilizzare un ciclo
whilecon un indice per gestire le modifiche alla lista in modo più controllato.
Vediamo ora un esempio di utilizzo dei cicli for sulle liste. Supponiamo di voler calcolare la media dei valori contenuti in una lista di dati sperimentali. Data la lista
dati_sperimentali = [1, 34, 2, -4, 6, 8]
cerchiamo di calcolare la media dei suoi elementi.
Il primo metodo è quello "classico", basato sul fatto che ogni elemento della lista ha un indice ad esso associato, per cui basta scorrere gli indici con range.
dati_sperimentali = [1,34, 2,-4, 6,8]
media = 0
n_dati = len(dati_sperimentali) # numero elementi nella lista
for i in range(n_dati): # da 0 a n-1
media += dati_sperimentali[i]
media = media / n_dati
print(f"La media dei dati sperimentali è {media}")
La media dei dati sperimentali è 7.833333333333333
Il secondo è proprio di Python e altri linguaggi di livello molto alto e consiste nel ciclare direttamente gli elementi della lista
dati_sperimentali = [1,34, 2,-4, 6,8]
media = 0
for valore in dati_sperimentali:
media += valore
media = media / len(dati_sperimentali)
print(f"La media dei dati sperimentali è {media}")
La media dei dati sperimentali è 7.833333333333333
Il secondo approccio è spesso preferibile quando lavori su una sola lista: è più leggibile e usa una variabile in meno, riducendo gli errori di distrazione (ad esempio riutilizzare il contatore i in modo scorretto).
Esercizio - Varianza in un solo ciclo¶
Calcola la varianza di un set di dati tramite la relazione $$\sigma^2 = \langle x^2 \rangle - \langle x \rangle^2 $$ dove $\langle x \rangle = \frac{1}{N} \sum_{i=1}^N x_i$ e $\langle x^2 \rangle = \frac{1}{N} \sum_{i=1}^N x_i^2$.
Prova ad usare un singolo ciclo
forper calcolare sia $\langle x \rangle$ che $\langle x^2 \rangle$ in un unico passaggio.
dati = [1, 34, 2, -4, 6, 8] # La varianza attesa è circa 151.47
IMPORTANTE: le liste sono mutabili¶
Poiché le liste contengono un insieme di valori, possono occupare molto spazio in memoria. Per questo motivo, quando si assegna una lista a una nuova variabile, Python non crea una nuova copia della lista, ma un nuovo riferimento alla stessa area di memoria (assegnamento per referenza, vedi slides del corso di teoria). Questo comportamento, unito al fatto che le liste sono mutabili, può portare a risultati inaspettati se non si è consapevoli di come funziona l'assegnamento in Python.
numeri_dispari = [2*x+1 for x in range(5)] # lista dei primi 5 numeri dispari
print(f"I primi {len(numeri_dispari)} numeri dispari sono: {numeri_dispari}\n")
# Assegnamo un nuovo nome alla lista
numeri_dispari_alias = numeri_dispari
print(f"numeri_dispari_alias = {numeri_dispari_alias}\n")
# Modifichiamo ora la lista tramite il nuovo nome
numeri_dispari_alias[0] = 99
print(f"Dopo aver modificato il primo elemento tramite numeri_dispari_alias, la lista è {numeri_dispari_alias=}\n")
print(f"Ma anche numeri_dispari è stata modificata: {numeri_dispari=}\n")
I primi 5 numeri dispari sono: [1, 3, 5, 7, 9] numeri_dispari_alias = [1, 3, 5, 7, 9] Dopo aver modificato il primo elemento tramite numeri_dispari_alias, la lista è numeri_dispari_alias=[99, 3, 5, 7, 9] Ma anche numeri_dispari è stata modificata: numeri_dispari=[99, 3, 5, 7, 9]
Le variabili numeri_dispari e numeri_dispari_alias nell'esempio qui sopra rappresentano quindi un collegamento che ci porta ai dati che ci interessano e quando facciamo un'assegnazione come numeri_dispari_alias=numeri_dispari stiamo copiando il collegamento e non i dati stessi. Per capire un po' meglio la cosa, immaginate che queste variabili siano un link ad un documento condiviso su google drive. Le liste sono oggetti mutabili, quindi chiunque abbia il link può modificare il documento e questo risulta poi modificato per tutti gli altri. Altri elementi di Python, ad esempio i numeri, i caratteri e le stringhe, sono invece immutabili: Python in questo caso crea una nuova copia del valore, quindi modificare una variabile non influirà sull'altra. Riprendendo l'esempio di prima è come se modificare il documento creasse automaticamente una nuova copia,lasciando immutato quello originale.
a = 0
b = 0
print(f"Prima della modifica: a={a}, b={b}")
b = 10
print(f"Dopo la modifica: a={a}, b={b}")
Prima della modifica: a=0, b=0 Dopo la modifica: a=0, b=10
Esercizio - Copia indipendente di una lista¶
Implementa un metodo per copiare una lista. Verifica che modificare la lista copiata non modifichi la lista originale. Nota: le liste in python hanno un metodo,
copy(), che permette di copiarle senza dover scorrere tutti gli elementi come in questo esercizio.
lista_da_modificare = [1, 2, 3, 4, 5]
# tuo codice
... # Chiama la nuova lista `lista_originale`
# fine tuo codice
lista_da_modificare[0] = 99
print(f"Dopo la modifica {lista_da_modificare=}")
#print(f"La lista originale è {lista_originale=}")
Dopo la modifica lista_da_modificare=[99, 2, 3, 4, 5]
Stringhe: una prima introduzione¶
Le stringhe servono per rappresentare parole, frasi e testi. Sono oggetti Python a tutti gli effetti il che implica che supportano operazioni avanzate come slicing, concatenazione, e molte funzioni predefinite. Possiamo crearle con virgolette singole/doppie oppure con triple virgolette: queste ultime sono comode per testi multilinea e docstring.
str1 = "parola"
str2 = "Un'altra parola. Nota l'apostrofo"
str3 = """
una stringa
su più
righe
"""
avviene_talvolta = """
Avviene talvolta
che il tempo trascorra remoto e leggero
come sugli alti torrioni
d’un medievale maniero,
allora tu non ricordi
più nulla della tua vita
e generosa ti involgi
dentro una rena sopita.
Avviene che il tempo talvolta dimentichi il tuo coraggio
e tu ne vieni sconvolta
come una rosa nel maggio.
(A. Merini)
"""
print(avviene_talvolta)
Avviene talvolta che il tempo trascorra remoto e leggero come sugli alti torrioni d’un medievale maniero, allora tu non ricordi più nulla della tua vita e generosa ti involgi dentro una rena sopita. Avviene che il tempo talvolta dimentichi il tuo coraggio e tu ne vieni sconvolta come una rosa nel maggio. (A. Merini)
Possiamo accedere a parti di una stringa usando lo slicing, così come per le liste:
print(avviene_talvolta[19:41])
che il tempo trascorra
A differenza delle liste però, le stringhe sono immutabili. Quindi un comando come il seguente
str1 = "ciao"
str1[0] = 'm'
restituisce un errore. Provateci!
f-strings¶
Una funzione avanzata delle stringhe sono le f-strings (formatted string literals) che abbiamo utilizzato più volte in questo notebook. Si creano anteponendo la lettera f alla stringa, e permettono di inserire espressioni Python all'interno di parentesi graffe {} che vengono valutate al momento dell'esecuzione della stringa.
a = 10
b = 20
str6 = f"a/2 = {a/2}, a*b = {a*b}"
Quello che succede è che il codice contenuto tra parentesi graffe viene eseguito prima di assegnare il valore alla variabile str6. Questo permette di scrivere stringhe che riportino i risultati di un calcolo in modo molto elegante.
Alcuni esempi avanzati delle f-strings includono:
- Presentazione del valore di una variabile con il suo nome:
f"{variabile=}"restituisce una stringa del tipo"variabile=valore". - Formattazione dei numeri: è possibile specificare il numero di cifre decimali da visualizzare per un numero in virgola mobile usando la sintassi
f"{variabile:.2f}", che restituisce una stringa con il valore divariabileformattato con due cifre decimali. Il numero dopo il punto indica il numero di cifre decimali e può essere qualsiasi numero intero, non necessariamente 2. - Formattazione dei numeri in notazione scientifica: è possibile formattare un numero in notazione scientifica usando la sintassi
f"{variabile:.4e}", che restituisce una stringa con il valore divariabileformattato in notazione scientifica con due cifre decimali. - Selezione di un numero minimo di caratteri: è possibile specificare un numero minimo di caratteri per la visualizzazione di una variabile usando la sintassi
f"{variabile:10}", che restituisce una stringa con il valore divariabileallineato a destra e con un minimo di 10 caratteri. Se il valore divariabileha meno di 10 caratteri, verranno aggiunti spazi vuoti a sinistra per raggiungere il numero minimo di caratteri. Se invece il valore divariabileha più di 10 caratteri, verrà visualizzato per intero senza troncamenti. È anche possibile specificare l'allineamento a sinistra o al centro usando rispettivamente<o^al posto di:10.
Potete trovare più informazioni sulle f-strings nella documentazione ufficiale. Per superare l'esame è sufficiente saper scegliere il numero di cifre decimali da visualizzare per un numero in virgola mobile f{1/3:.5f}ed il formato esponenziale f{1/3:.5e} ma è importante sapere che esistono anche altre funzionalità avanzate, in modo da poterle utilizzare quando ne avete bisogno.
a = 10
b = 20
stringa_multilinea_cifre_decimali = f"""
| Operazione | Risultato |
|------------|-----------|
| a/2 | {a/2:<10.2f}|
| a*b | {a*b:^10}|
| a%b | {a%b} |
| a**b | {a**b:>10.1e}|
| | {b:=.5e} |
"""
print(stringa_multilinea_cifre_decimali)
stringa_singla_linea = f"a/2 = {a/2:.2f}, a*b = {a*b}, a%b = {a%b}, a**b = {a**b:e}, b = {b:=.5e}"
print(stringa_singla_linea)
| Operazione | Risultato | |------------|-----------| | a/2 | 5.00 | | a*b | 200 | | a%b | 10 | | a**b | 1.0e+20| | | 2.00000e+01 | a/2 = 5.00, a*b = 200, a%b = 10, a**b = 1.000000e+20, b = 2.00000e+01
Esercizio - Formattazione con f-string¶
Definisci due variabili numeriche
aebe costruisci unaf-stringche riporti:
- il valore di
a+b,- il valore di
a*b,- il rapporto
a/bcon 2 cifre decimali.- il rapporto
a/bin notazione scientifica con 1 cifra decimale.
Breve digressione su print¶
print è una funzione. La maggior parte della funzioni sono come quelle matematiche: prendono degli input, eseguono delle operazioni su di essi, e restituiscono un output che può essere salvato su una variabile (es: la funzione len()). Altre funzioni invece eseguono un algoritmo il cui output consiste in qualche effetto osservabile che non può essere salvato in una variabile. Ad esempio, print è una funzione speciale che invece di restituire un output salvabili in una variabile produce un effetto osservabile: stampa a schermo il suo argomento. Infine, esistono funzioni che modificano il contenuto di una variabile, ad esempio append che aggiunge un elemento alla fine di una lista.
In generale, si può pensare a una funzione come ad una scatola nera che implementa un algoritmo: ricevuti degli input, la scatola esegue un algoritmo al suo interno, e o restituisce un output che può essere salvato in una variabile oppure produce un effetto osservabile. Non è necessario sapere come funziona la scatola per usarla, basta sapere quali input accetta e quali effetti/output produce.
Per sapere quali sono gli input accettati da una funzione, si può consultare la documentazione ufficiale di Python o scrivere il nome della funzione seguito da ? e premere Shift + Enter.
A titolo di esempio andiamo a vedere la documentazione di print.
print?
Esercizio - Stampa su una sola riga¶
Ogni volta che utiliziamo la funzione
for i in range(1, 6):
# Utilizza print per stampare tutti i numeri sulla stessa riga, separati da uno spazio
Cell In[35], line 2 # Utilizza print per stampare tutti i numeri sulla stessa riga, separati da uno spazio ^ SyntaxError: incomplete input
Riepilogo¶
In questo notebook abbiamo visto:
- caratteristiche principali di Python (interpretato, dinamico, alto livello);
- tipi base, operatori e precedenza;
- controllo del flusso con
if,while,for; - liste, slicing e cicli sugli elementi;
- stringhe, immutabilità e
f-strings; - comportamento delle variabili con oggetti mutabili e passaggio per riferimento.
Risorse per approfondire¶
- Documentazione ufficiale Python: https://docs.python.org/3/
- Tutorial ufficiale: https://docs.python.org/3/tutorial/
- Guida rapida Jupyter: https://jupyter.org/
- NumPy (base scientifica): https://numpy.org/learn/