Python: introduzione alla programmazione procedurale e a oggetti¶
Nelle lezioni precedenti abbiamo presentato il paradigma della programmazione imperativa in cui le istruzioni sono eseguite in sequenza una dopo l'altra. Vogliamo ora introdurre due nuovi paradigmi: la programmazione procedurale e la programmazione orientata agli oggetti.
Nel primo paradigma, le sequenze di istruzioni sono raggruppate in procedure (routine o funzioni) che ricevono un input e generano un output determinato. Grazie a routine e funzioni è possibile scrivere pezzi di codice facilmente riutilizzabili in programmi diversi e che possono essere testati e mantenuti in modo indipendente. Questo paradigma si concentra sulla scomposizione di un programma in unità funzionali e su come i vari dati vengono trasformati da un'unità all'altra.
La programmazione orientata agli oggetti invece, è un paradigma che organizza il software come una collezione di entità che interagiscono tra loro. Queste entità contengono sia dati che funzioni per lavorare su di essi, e in diversi casi permettono di isolare meglio la logica del programma in pezzi riutilizzabili. Gli oggetti rappresentano inoltre nuovi tipi di dato che possono essere passati a funzioni e restituiti da queste e soprattutto su cui gli operatori aritmetici opereranno in modo specifico.In Python, la logica di questi oggetti è rappresentata da classi. Queste specificano che tipo di dati contengono (i loro attributi) e le funzioni per lavorare su queste (metodi), ma non specificano i dati effettivamente contenuti. Le classi si usano per creare delle istanze che sono loro realizzazioni con attributi specificati. Facciamo un esempio per chiarire la cosa. Pensiamo ad un numero complesso, $x=a + ib$. Una classe che lo implementi dovrebbe contenere i valori $a$ e $b$ e implementare somma, moltiplicazione, etc. tra numeri complessi. Potrebbe anche contenere dei metodi per calcolare il modulo del numero complesso, il suo coniugato, etc. Un' istanza di questa classe è una variabile contenente un numero complesso preciso, ad esempio $2 + i4$.
Obiettivi formativi¶
- Comprendere come definire e usare funzioni in Python
- Imparare a documentare le funzioni in modo chiaro
- Capire il concetto di variabili globali e scope (visibilità)
- Riconoscere gli oggetti e le loro proprietà (attributi, metodi, operatori)
- Utilizzare metodi di liste e stringhe
- Applicare il concetto di overloading degli operatori
Funzioni in Python¶
Le funzioni in programmazione sono blocchi di codice che eseguono un compito specifico e possono essere riutilizzati in diverse parti di un programma. La maggior parte delle funzioni che andremo a scrivere sono come le funzioni matematiche: prendono un input (chiamato argomento) e restituiscono un output (chiamato valore di ritorno). Tuttavia, esistono anche funzioni dette procedure che non restituiscono un valore ma eseguono un'azione specifica, come ad esempio stampare qualcosa a schermo o modificare un oggetto mutabile passato come argomento.
Python fornisce un'ampia gamma di funzioni, sia nel linguaggio base (ad es. len, print,...) e nella libreria standard, come ad esempio il supporto per funzioni matematiche avanzate (math.sqrt, math.sin, ecc. come vedremo in seguito). È inoltre possibile installare librerie esterne che forniscono funzioni specifiche per compiti particolari, come ad esempio numpy per il calcolo scientifico o matplotlib per la visualizzazione grafica.
È possibile definire le proprie funzioni in Python usando la parola chiave def, seguita dal nome della funzione e da una coppia di parentesi che possono contenere parametri. Il corpo della funzione è indentato rispetto alla definizione. Le funzioni possono restituire un valore usando la parola chiave return.
Attenzione: quando una funzione ritorna (cioè esegue la parola chiave return) l'esecuzione della funzione termina e il controllo torna al punto in cui la funzione è stata chiamata, restituendo il valore specificato dopo return. Qualsiasi operazione scritta dopo return non viene eseguita.
def nome_funzione(parametro1, parametro2, ..., parametroN):
"""Docstring che descrive la funzione e i suoi parametri
Questa stringa viene usata come documentazione della funzione e può essere
visualizzata usando la funzione `help(nome_funzione)` o `nome_funzione?`
in un notebook Jupyter.
"""
# Corpo della funzione
...
return risultato
ad esempio:
def f(a, b):
"""La mia funzione"""
return a + b
Tuttavia la potenzialità delle funzioni sta nel poter riutilizzare e condividere il codice, quindi conviene sempre utilizzare nomi descrittivi per le funzioni e per i loro parametri, in modo da rendere chiaro a chi legge il codice cosa fa la funzione e quali sono i suoi input e output. In questo modo si facilita la comprensione da parte di altri ricercatori (o da noi stessi in futuro) e si riduce il rischio di errori o malintesi. Una versione migliore della funzione precedente potrebbe essere:
def somma(a, b):
"""Restituisce la somma di a e b
Args:
a : il primo valore da sommare
b : il secondo valore da sommare
Returns:
float: la somma di a e b
"""
return a + b
Finché le funzioni sono semplici, è facile capire cosa fanno e come usarle. Ma quando le funzioni diventano più complesse, con molti parametri o con un comportamento non banale, è importante documentarle in modo chiaro e completo, in modo da facilitarne la comprensione e l'uso corretto da parte di altri ricercatori (o da noi stessi in futuro).
Esercizio Avanzato - Funzione Sconosciuta¶
Provate a capire cosa fa la seguente funzione e a scrivere un esempio di input e output. Può aiutarvi eseguirla con diversi input per capire meglio il suo comportamento. Una volta capito cosa fa, provate a scrivere una docstring che spieghi in modo chiaro e completo cosa fa la funzione, quali sono i suoi parametri e il suo output.
def f(n): i = 1/n e = 1 for _ in range(n): e *= (1 + i) return e
Cerchiamo ora di scrivere una funzione che calcoli la somma di tutti gli elementi contenuti in una lista.
Per fare ciò, conviene ragionare su alcuni punti:
- Per calcolare la somma di tutti gli elementi di una lista è necessario scorrerli tutti in successione. Un ciclo
forè un modo semplice per farlo. Non abbiamo bisogno di modificare gli elementi quindi basta unfor elemento in listasenza ricorrere afor i in range(len(lista)). - La somma di una lista è un numero che viene aggiornato ad ogni iterazione del ciclo. Quindi è necessario inizializzare una variabile
sprima del ciclo. - Se per qualche motivo la lista dovesse essere vuota, la somma dovrebbe essere 0. Inizializziamo quindi
sa 0 prima del ciclo. - Ad ogni iterazione del ciclo andremo a sommare un valore ad
s. Possiamo quindi usare l'operatore+=invece dis = s + xper rendere il codice più conciso. - Vogliamo restituire la somma calcolata alla fine della funzione, quindi è necessario usare la parola chiave
returnper restituire il valore disal chiamante. - Per documentare la funzione, scriviamo una breve docstring, scegliamo un nome autoesplicativo e utilizziamo le iniziali delle variabili per indicare il loro significato (ad esempio
sper somma). A scanso di equivoci, specifichiamo il nome completo quando creiamo la variabile
def somma_elementi(l):
"""Restituisce la somma degli elementi contenuti in l.
Args:
l: una lista di numeri
Returns:
La somma degli elementi contenuti in l.
"""
s = 0 # `s` sta per somma
for x in l:
s += x
return s
# `x` rappresenta l'elemento generico,
# la `s` in fondo indica il plurale in
# inglese. `xs` sta per `le x`
xs = [1, 2, 3, 4]
# `s` sta per somma, è il risultato della funzione
s = somma_elementi(xs)
print(s)
10
Esercizio - Media di una lista di numeri¶
Scrivere una funzione che calcoli la media dei valori contenuti in una lista.
Esercizio - Numeri Pari¶
Scrivere una funzione che prenda in input una lista di numeri e restituisca una nuova lista contenente solo i numeri pari.
Funzioni che non ritornano nulla: routine¶
In Python è anche possibile definire routine, cioè funzioni che non ritornano un valore. Un esempio di routine è la funzione print che abbiamo utilizzato più volte per stampare a schermo dei risultati. In alternativa, le routine possono anche modificare oggetti mutabili passati come argomento.
Le routine si definiscono come funzioni normali ma si omette la parola chiave return.
def mia_routine(parametro1, parametro2):
# codice che esegue operazioni su parametro1 e/o parametro2
Attenzione: se non è presente un return, Python introduce automaticamente un return None per indicare che la funzione non restituisce valori utili. Questo può portare a comportamenti inaspettati se ci si dimentica di inserire la parola chiave return in una funzione che dovrebbe restituire un risultato.
Facciamo ora l'esempio di due funzioni. La prima stampa la somma dei valori in una lista, la seconda modifica la lista raddoppiando tutti i suoi elementi.
def stampa_somma(l):
"""
Stampa la somma dei valori in una lista.
Args:
l: una lista di numeri
Return:
None
"""
s = 0
for x in l:
s += x
print(f"La somma è {s}")
# se non scrivete return, Python aggiunge questa riga implicitamente: return None
stampa_somma(xs)
La somma è 10
Come vengono passati i dati alle funzioni?¶
Come accennato nella lezione precedente, Python cerca quando possibile di ottimizzare l'utilizzo di memoria. Per questo motivo i parametri di una funzione possono venir passati in due diversi modi a seconda del tipo di dato che stiamo passando.
- se l'oggetto è mutabile (ad esempio una lista), modificarlo dentro la funzione modifica anche l'oggetto visto all'esterno (come se stessi modificando un documento online condiviso con altri).
- se l'oggetto è immutabile (ad esempio numeri e stringhe), le operazioni producono un nuovo oggetto e il valore esterno non cambia automaticamente (se invio il pdf di un documento a qualcuno, il mio documento non cambia se l'altra persona modifica il pdf).
Per capire meglio questo concetto, proviamo a scrivere una funzione che prende in input una lista di numeri e la modifica raddoppiando tutti i suoi elementi.
def raddoppia_lista(lista_input):
n = len(lista_input)
for i in range(n):
lista_input[i] *= 2
a = [2, 3, 4]
print(f"La lista a prima di chiamare raddoppia_lista è {a}")
raddoppia_lista(a)
print(f"""
La lista a dopo aver chiamato raddoppia_lista è {a}.
La funzione ha quindi modificato il valore di `a` senza restituirlo.
""")
La lista a prima di chiamare raddoppia_lista è [2, 3, 4] La lista a dopo aver chiamato raddoppia_lista è [4, 6, 8]. La funzione ha quindi modificato il valore di `a` senza restituirlo.
b = [1, 2, 3]
print(f"Definisco una lista {b=}")
c = b
print(f"""Ho provato a copiare `b` in `c` usando l'operatore di assegnamento `=`.
`c` sembra essere una copia di `b`: {c=}.
""")
raddoppia_lista(b)
print(f"""
La lista a dopo aver chiamato raddoppia_lista è {b=}.
La lista `c` è stata modificata anche se non l'ho passata alla funzione: {c=}.
""")
# Questo accade perchè `b` e `c` sono due "puntatori" che puntano allo stesso
# oggetto in memoria come spiegato nella lezione precedente.
Definisco una lista b=[1, 2, 3] Ho provato a copiare `b` in `c` usando l'operatore di assegnamento `=`. `c` sembra essere una copia di `b`: c=[1, 2, 3]. La lista a dopo aver chiamato raddoppia_lista è b=[2, 4, 6]. La lista `c` è stata modificata anche se non l'ho passata alla funzione: c=[2, 4, 6].
Esercizio - Copia di una lista¶
Scrivi una funzione
copiache prenda in input una lista e restituisc una nuova lista che contiene gli stessi elementi della lista originale. La funzione deve essere implementata in modo tale che una modifica della lista restituita non modifichi la lista originale.
Attenzione: non basta copiare un riferimento alla lista originale dentro la funzione, altrimenti si avrebbe lo stesso problema di prima. Bisogna invece creare una nuova lista e copiare gli elementi uno ad uno.
Proviamo ora a modificare il valore di un oggetto immutabile passato come argomento.
def append_to_list(l, x, n):
"""Aggiunge l'elemento `x` alla lista `l` e prova a modificare la lunghezza `n` della lista
Args:
l: una lista (che verrà passata per referenza/come oggetto mutabile)
x: un elemento da aggiungere alla lista
n: la lunghezza della lista `l` (che verrà passata per valore essendo immutabile)
Returns:
n: la lunghezza della lista `l` dopo l'aggiunta di `x`
"""
l.append(x) # Modifico la lista `l` (passata per referenza)
n += 1 # Modifico `n` (passato per valore)
return n
l = [1, 2, 3]
n = len(l)
print(f"La lista `l` prima di chiamare `append_to_list` è {l=}, la sua lunghezza è {n=}")
# Nuova lunghezza
new_n = append_to_list(l, 4, n)
print(f"La lista `l` dopo aver chiamato `append_to_list` è {l=}, la sua lunghezza è {len(l)=} (modificata da `append_to_list`)")
print(f"La variabile {n=} non è stata modificata da `append_to_list`")
print(f"Mentre {new_n=} è stata modificata e ritornata da `append_to_list` correttamente")
La lista `l` prima di chiamare `append_to_list` è l=[1, 2, 3], la sua lunghezza è n=3 La lista `l` dopo aver chiamato `append_to_list` è l=[1, 2, 3, 4], la sua lunghezza è len(l)=4 (modificata da `append_to_list`) La variabile n=3 non è stata modificata da `append_to_list` Mentre new_n=4 è stata modificata e ritornata da `append_to_list` correttamente
Esercizio - Ordinamento di una lista¶
Scrivere una funzione che riordini una lista dal valore più piccolo al più grande. Come riferimento potete usare l'algoritmo di ordinamento a bolle (bubble sort) che funziona confrontando ogni elemento della lista con il successivo e scambiandoli se sono nell'ordine sbagliato. Questo processo viene ripetuto fino a quando la lista è ordinata.
# Scrivere qui la soluzione
Attenzione allo scope: visibilità delle variabili¶
In Python, le variabili definite al livello globale (cioè fuori dalle funzioni) sono visibili anche all'interno di queste, il che può generare errori subdoli e difficili da individuare.
x = 5
def somma(y):
"""
Somma il valore di `x` con `y`.
Args:
y: un numero da sommare a `x`
Returns:
La somma di `x` e `y`
Warning:
`x` è una variabile globale. Ho dimenticato di passarla come argomento
"""
return x + y
print(f"La somma di {x=} (globale) e y=1 (passato come argomento) è {somma(1)}")
La somma di x=5 (globale) e y=1 (passato come argomento) è 6
In altre parole, le variabili definite fuori dalle funzioni sono visibili anche all'interno di queste. Ad esempio, nell'esempio precedente, la variabile x non è definita nella funzione, ma è comunque visibile all'interno di essa quindi Python non genera un errore.
Questo comportamento, unito al fatto che il notebook mantiene in memoria anche le variabili che sono state cancellate, spiega perché è importante riavviare il kernel periodicamente per evitare errori subdoli e comportamenti inaspettati.
Un esempio analogo è la seguente funzione sommatoria_sbagliata in cui abbiamo dimenticato di definire s ma abbiamo una variabile con lo stesso nome disponibile a livello globale:
def sommatoria_sbagliata(l):
"""Calcola la somma degli elementi in `l` partendo da una variabile `s` che non è definita all'interno della funzione.
Args:
l: una lista di numeri
Returns:
La somma degli elementi in `l` più il valore di `s` (che non è definito all'interno della funzione)
Warning:
`s` è una variabile globale.
"""
# Nota che la cella viene eseguita, e la funzione definita, anche
# se non sappiamo cosa sia s!
return s + somma_elementi(l)
Nell'esempio seguente sommatoria_sbagliata dà il risultato atteso, ma solo perché abbiamo definito s prima di eseguirla.
lista = [1,2,4,3]
s = somma_elementi(lista)
print(f"La somma degli elementi di lista è {s=}")
s1 = sommatoria_sbagliata(lista)
print(f"La somma con s implicito tramite `sommatoria_sbagliata` vale {s1}")
La somma degli elementi di lista è s=10 La somma con s implicito tramite `sommatoria_sbagliata` vale 20
Se non definiamo la variabile s nemmeno nello scope globale o la cancelliamo dalla memoria tramite la parola chiave del, l'interprete Python non riuscirà a trovare la variabile s e genererà un errore di tipo NameError
# Rimuovete i commenti per osservare il NameError su `s`
#del(s)
#print(sommatoria_sbagliata(lista))
Linee guida per scrivere buone funzioni¶
In generale, nelle funzioni non vanno mai lasciate variabili che dipendono da una variabile esterna. Queste infatti legano il codice dentro la funzione a quello all'esterno di questa, rendendo vano il nostro tentativo di utilizzare le funzioni per scrivere codice riutilizzabile e mantenibile.
Per evitare errori e rendere il codice più robusto, seguire queste accortezze:
- Non usare mai variabili esterne, passarle piuttosto come argomento.
- Eliminare le variabili inutili. Meno variabili ci sono, meno problemi posso avere.
- Evitare nomi facilmente confondibili. Ad esempio
neN: Python è case-sensitive, per cui queste sono variabili diverse. Meglio scegliere nomi più descrittivi, come ad esempioelementoenumero_elementi_totali. - Rispettate le convenzioni di denominazione. Ad esempio, per le funzioni è buona pratica usare nomi in minuscolo con parole separate da underscore (ad esempio
calcola_somma), mentre per le variabili è buona pratica usare nomi in minuscolo senza underscore (ad esempiosomma).
# 1. Se mi serve una variabile esterna, la passo come argomento
def somma_elementi_a_s(s, array):
"""
Calcola la somma degli elementi di un array aggiungendo un valore iniziale.
Args:
s (float): Valore iniziale.
array (list): Lista di numeri.
Returns:
float: Somma degli elementi dell'array e del valore iniziale.
"""
n = len(array)
x = s # ora va tutto bene perché s è un argomento
for i in range(n):
x = x + array[i]
return x
# 2. Non aggiungo variabili inutili! Nell'esempio qui sopra posso usare direttamente s, senza creare anche x!
# Meno variabili ci sono, meno problemi posso avere.
def somma_elementi_a_s(s, array):
"""
Calcola la somma degli elementi di un array aggiungendo un valore iniziale.
Args:
s (float): Valore iniziale.
array (list): Lista di numeri.
Returns:
float: Somma degli elementi dell'array e del valore iniziale.
"""
n = len(array)
for i in range(n):
s = s + array[i]
return s
s = somma_elementi_a_s(0,[1,2,3,4])
print(f"Somma corretta: {s}")
Somma corretta: 10
La seguente funzione contiene un errore dovuto al fatto che abbiamo scritto n in un punto e N nell'altro. Come detto, Python è case-sensitive, per cui queste sono variabili diverse. Se siamo sfortunati, N esiste al di fuori della funzione, per cui la sua esecuzione non causa nessun errore esplicito... ma dà comunque il risultato sbagliato.
# 3. Prestate particolare attenzione ai valori che passate a range!! Un esempio:
def somma_elementi_a_s_sbagliata(s, array):
n = len(array)
for i in range(N): #N è un'altra variabile perché Python è case-sensitive
s = s + array[i]
return s
N = 2
X = [1,2,3,4]
print(f"Somma con errore subdolo: {somma_elementi_a_s_sbagliata(0,X)}")
# in questo caso il for nella funzione è eseguito sempre e solo sui primi due elementi, indipendentemente
# dalla lunghezza dell'array.
Somma con errore subdolo: 3
Notate che il modo migliore per scrivere una funzione come quella vista qui sopra è quello che non necessita di n:
def somma_elementi_a_s_B(s0,array):
s = s0
for x in array:
s+=x
return s
X = [1,2,3,4]
print(f"Somma corretta: {somma_elementi_a_s_B(0,X)}")
Somma corretta: 10
Documentare le funzioni¶
Per completezza, vediamo una buona pratica su come documentare le funzioni in modo chiaro ed efficace. Ci conviene scrivere una docstring essenziale ma completa che spieghi cosa fa la funzione, quali sono i parametri di input e cosa restituisce. Quello che scriviamo nella docstring è il testo che viene poi stampato dall'interprete quando chiamiamo la funzione con nomefunzione?. Qui sotto vediamo un possibile esempio, basato sullo stile di numpy.
Potete trovare una spiegazione estesa sulla convenzione per le docstring nel sito ufficiale di numpy https://numpydoc.readthedocs.io/en/latest/format.html.
#-- Documentare le funzioni
def somma_elementi_a_s(s,array):
"""
Somma gli elementi di array tra loro e li aggiunge ad s
Input
------
s: variabile scalare
array: array di numeri da sommare
Return
-------
x scalare
"""
n = len(array)
#s = 0 # ATTENZIONEEEE
x = s
for i in range(n):
x = x + array[i]
# se non scrivete return, Python aggiunge `return None` implicitamente
return x
# il punto di domanda fa sì che l'interprete stampi la docstring.
somma_elementi_a_s?
Esercizio - Varianza ben documentata¶
Copiate la funzione per la varianza vista nella lezione precedente e scrivete una docstring che spieghi in modo chiaro e completo cosa fa la funzione, quali sono i suoi parametri di input e cosa restituisce.
Python e la programmazione a oggetti¶
Python è un linguaggio di programmazione a oggetti. Gli oggetti (istanze di classi in Python) sono tipi di dati speciali che possono essere creati dal programmatore. Questi nuovi tipi di dati permettono sia di generalizzare gli operatori del linguaggio (ad es. + - * /), sia di tenere insieme dati e funzioni che lavorano su di essi. Nella programmazione a oggetti, questi sono i nostri blocchi Lego con cui costruire programmi più complessi.
Le liste sono classi¶
Le liste sono classi, e come tali hanno
- degli attributi, in questo caso i valori salvati in esse;
- degli operatori,
+,*, specializzati per lavorare su di esse; - dei metodi, cioè funzioni definite nella classe stessa.
Una nota di linguaggio: una classe è il tipo di dato, un'istanza è una sua realizzazione.
Ad esempio, tutte le liste che abbiamo creato finora sono istanze della classe list ed il loro comportamento è definito dalla classe list, ma il contenuto di ogni lista è diverso.
l1 = [1, 2, 3, 4]
l2 = [10, 20, 30]
print(f"La classe di l1 è {type(l1)}, così come la classe di l2 è {type(l2)}")
print(f"Quindi possiamo usare gli stessi operatori, come ad esempio lo slicing: {l1[1:3]=}, {l2[1:2]=}")
print(f"Tuttavia le istanze sono però diverse: {l1=}, {l2=}")
l2 = list(range(10))
La classe di l1 è <class 'list'>, così come la classe di l2 è <class 'list'> Quindi possiamo usare gli stessi operatori, come ad esempio lo slicing: l1[1:3]=[2, 3], l2[1:2]=[20] Tuttavia le istanze sono però diverse: l1=[1, 2, 3, 4], l2=[10, 20, 30]
Una particolarità delle classi è che possono fornire modi per convertire un tipo in un altro. Ad esempio, la funzione range restituisce un oggetto di tipo range, ma è possibile convertirlo in una lista usando la funzione list.
r = range(5)
print(f"Il tipo di r è {type(r)} e il suo valore è {r} (che è diverso da una lista!)")
# Conversione di range a lista
r_to_l = list(r)
print(f"Il tipo di r_to_l è {type(r_to_l)} e il suo valore è {r_to_l} (che è una lista!)")
Il tipo di r è <class 'range'> e il suo valore è range(0, 5) (che è diverso da una lista!) Il tipo di r_to_l è <class 'list'> e il suo valore è [0, 1, 2, 3, 4] (che è una lista!)
Oppure, potremmo voler convertire una lista in un insieme set per eliminare i duplicati (ma rinunciare all'ordine di inserimento). In questo caso, possiamo usare la funzione set per convertire la lista in un insieme.
l = [1, 2, 6, 3, 2, 4, 1, 2]
print(f"Il tipo di l è {type(l)} e il suo valore è {l}")
s = set(l)
print(f"Il tipo di s è {type(s)} e il suo valore è {s} (che è un insieme, non una lista!)")
Il tipo di l è <class 'list'> e il suo valore è [1, 2, 6, 3, 2, 4, 1, 2]
Il tipo di s è <class 'set'> e il suo valore è {1, 2, 3, 4, 6} (che è un insieme, non una lista!)
Oppure potremmo convertire una lista in una stringa (che poi è quello che fa la funzione print)
l = [1, 2, 4, 6, 7]
l_to_str = str(l)
print(f"Il tipo di l è {type(l)} e il suo valore è {l}")
print(f"Il tipo di l_to_str è {type(l_to_str)} e il suo valore è {l_to_str}")
Il tipo di l è <class 'list'> e il suo valore è [1, 2, 4, 6, 7] Il tipo di l_to_str è <class 'str'> e il suo valore è [1, 2, 4, 6, 7]
Operatori: sommare e moltiplicare liste¶
Siccome le classi permettono di definire un nuovo tipo di dato e di specificare il suo comportamento, Python fornisce la possibilità di ridefinire gli operatori aritmetici per lavorare con questi nuovi tipi di dati. Ad esempio, se abbiamo due liste a e b, possiamo usare l'operatore + per concatenarle, e l'operatore * per ripeterle un certo numero di volte.
l1 = [1, 2, 3]
l2 = [4, 5, 6]
l3 = l1 + l2
print(f"l1 + l2 = {l1} + {l2} = {l3}")
l1 + l2 = [1, 2, 3] + [4, 5, 6] = [1, 2, 3, 4, 5, 6]
zeroes = [0]*10
print(zeroes)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Attenzione: il motivo per cui insistiamo a chiamare le liste "liste" è proprio per il loro comportamento sotto gli operatori di somma e moltiplicazione. La somma di due liste non è una lista ottenuta tramite la somma degli elementi così come il prodotto per scalare di una lista non è il prodotto delle sue componenti. Nel caso degli array di numpy invece, il comportamento è quello di un vettore matematico:
import numpy as np
v1 = np.array([1, 2, 3])
v2 = np.array([4, 5, 6])
v3 = v1 + v2
print(f"v1 + v2 = {v1} + {v2} = {v3}")
print(f"v1 * 2 = {v1} * 2 = {v1*2}")
print(f"Il tipo di v1, v2, e v3 è {type(v1)}")
v1 + v2 = [1 2 3] + [4 5 6] = [5 7 9] v1 * 2 = [1 2 3] * 2 = [2 4 6] Il tipo di v1, v2, e v3 è <class 'numpy.ndarray'>
La possibilità di ridefinire gli operatori aritmetici è chiamata overloading degli operatori ed è una caratteristica fondamentale della programmazione orientata agli oggetti. Grazie a questa possibilità, possiamo definire il comportamento di un nuovo tipo di dato in modo intuitivo e coerente con le nostre aspettative, rendendo il codice più leggibile e facile da usare.
Se la possibilità di ridefinire il comportamento degli operatori aritmetici vi sembra strana, pensate all'operazione di prodotto fra matrici. Se moltiplichiamo due matrici, non stiamo moltiplicando elemento per elemento, ma stiamo eseguendo un'operazione più complessa che coinvolge la somma dei prodotti degli elementi delle righe e delle colonne.
Metodi delle liste¶
Come sempre, scrivendo il nome di un'istanza di una classe seguito da . e premendo Tab l'interprete riporterà i metodi a nostra disposizione.
Alcuni particolarmente utili per le liste sono i seguenti.
append()aggiunge elementi in coda ad una lista: è utile per farla crescere in un ciclo.copy()crea una copia indipendente di una lista.sort()riordina la lista in modo che i suoi elementi siano in ordine dal più piccolo al più grande.reverse()inverte l'ordine degli elementi.
l2 = []
for x in range(10):
if x%2==0:
l2.append(x**2)
print(f"l2 contiene i quadrati dei numeri pari tra 0 e 10 escluso: {l2}")
l2.reverse()
print(f"l2 ora è in ordine inverso: {l2}")
l2 contiene i quadrati dei numeri pari tra 0 e 10 escluso: [0, 4, 16, 36, 64] l2 ora è in ordine inverso: [64, 36, 16, 4, 0]
Il metodo copy ci permette di creare una copia di una lista in modo da non modificare l'originale. Può essere molto utile all'interno di una funzione, ad esempio se vogliamo scrivere un ordinamento che non modifichi la lista originale ma ne restituisca una nuova.
l2 = [1,2,3,4]
l3 = l2.copy()
l3[0] = "Ciao"
print("l3 ora contiene:", l3)
print("l2 invece:", l2)
l3 ora contiene: ['Ciao', 2, 3, 4] l2 invece: [1, 2, 3, 4]
Esercizio - Sort non modificante¶
- Scrivete una funzione che riordini una lista dal numero più piccolo al più grande senza modificarla, ma restituendo una nuova lista ordinata. Usare al suo interno il metodo
.sort()o una funzione di ordinamento definita in precedenza.- Scrivete poi una funzione che, data una lista, restituisca una nuova lista con gli stessi elementi ma ordinati dal più grande al più piccolo.
- Provate ad ottenere lo stesso risultato della funzione precedente ma
Spoiler
senza usare la funzione `.reverse()`. Può aiutarvi leggere la documentazione di `sort` tramite `list.sort?`
Le stringhe sono classi¶
Le stringhe sono un altro esempio di classe in Python. Come le liste, anche le stringhe hanno attributi, operatori e metodi specifici. In particolare, le stringhe rappresentano un tipo di dato in più rispetto a quelli essenziali visti a lezione, cioè delle sequenze (immutabili) di caratteri. Anche in questo caso, l'overloading degli operatori permette di definire un comportamento specifico per gli operatori aritmetici quando vengono usati con le stringhe. Ad esempio, l'operatore + viene usato per concatenare due stringhe, mentre l'operatore * viene usato per ripetere una stringa un certo numero di volte.
Vediamo qualche esempio
str1 = "ciao"
str2 = " sono io" #lo spazio ci tornerà utile poi
print(f"{type(str1)=}, {type(str2)=}")
type(str1)=<class 'str'>, type(str2)=<class 'str'>
str1 e str2 ora sono due istanze della classe stringa (str). Hanno gli stessi metodi, ma il loro stato interno è diverso: "ciao" per str1 e " sono io" per str2. Possiamo usare vari metodi di queste per trasformare questo stato interno. Alcuni metodi disponibili sono:
capitalize(): metti la prima lettera maiuscola.upper(): tutto maiuscolo.lower(): tutto minuscolo.split(): separa la stringa in una lista di parole (o in base ad un altro separatore).
print(str1.upper(),str1.capitalize())
CIAO Ciao
parole2 = str2.split()
for p in parole2:
print(p)
sono io
Esercizio - Metodi delle stringhe¶
Data la stringa
"Ciao mondo da Python!", usare i metodi disponibili di stringa per:
- Convertire tutto in minuscolo
- Dividere la stringa in una lista di parole
- Rimpiazzare "Python" con "C" (hint: cercare il metodo
.replace())
Visto che le stringhe sono classi base di Python, possiamo contare sul fatto che alcuni operatori siano stati specializzati per lavorare su di esse. Ad esempio:
+concatena due stringhestr * intripete la stringanumvolte
Come nel caso dei numeri, il risultato di un'operazione simile è un'altra stringa: non vengono modificati gli operandi, ma viene restituito un risultato che va salvato in una nuova variabile.
str3 = str1 + str2
print(str3)
ciao sono io
str4 = str1*10
print(str4)
ciaociaociaociaociaociaociaociaociaociao
La precedenza degli operatori resta la stessa di prima!
str5 = str1 +2*str2
print(str5)
ciao sono io sono io
Esercizio - Estrazione dei cognomi¶
Si supponga di avere una stringa contenente nomi e cognomi, disposti in colonna come segue:
nome_1 cognome_1 nome_2 cognome_2 ... nome_n cognome_nScrivere una funzione che stampi solo i cognomi.
nomi_e_cognomi = """
Mario Rossi
Luigi Mario
Giorno Giovanna
Gabriel Archangel
Samuel Hayden
Riebeck Timberhearth
Cave Johnson
"""
# Il tuo codice
Esercizio - Estrazione delle parole finali¶
Si supponga di avere il testo di una poesia, come "avviene talvolta" di Ada Merini.
Scrivere una funzione che stampi solo le parole che terminano ogni riga. Il carattere di fine riga è identificato dal codice\nche possiamo passare ai metodi della classestr.Hint: usare il metodo
splitdopo averne studiato la docstring. Oppure studiare le docstring dei metodi di stringa per vedere se ce n'è qualcuno di utile.
Riepilogo¶
In questo notebook abbiamo esplorato i fondamenti della programmazione procedurale e a oggetti in Python:
Funzioni¶
- Le funzioni incapsulano algoritmi e rendono il codice riutilizzabile
- La documentazione tramite docstring è essenziale per la chiarezza
- Attenzione allo scope delle variabili: preferire sempre parametri espliciti piuttosto che variabili globali
- I dati vengono passati per referenza: modifiche ad oggetti mutabili (e.g. liste) dentro una funzione influenzano l'originale
Programmazione a oggetti¶
- In Python tutto è un oggetto: liste, stringhe, numeri sono istanze di classi
- Gli oggetti hanno attributi (dati), metodi (funzioni), e operatori specializzati
- Overloading degli operatori: gli operatori
+,*, ecc. sono ridefiniti per ogni tipo di dato - Imparare i metodi disponibili è fondamentale (usare
.method?o consultare la documentazione)
Buone pratiche¶
- Date nomi chiari alle funzioni e alle variabili
- Non usate variabili globali: passatele come argomenti
- Documentate le funzioni con docstring esplicite
- Prestate attenzione alla mutabilità: liste e dizionari vengono modificati, stringhe e numeri no
Suggerimenti per l'approfondimento¶
Risorse esterne¶
- Documentazione ufficiale di Python: https://docs.python.org/3/
- Built-in functions: https://docs.python.org/3/library/functions.html
- String methods: https://docs.python.org/3/library/stdtypes.html#string-methods
- List methods: https://docs.python.org/3/tutorial/datastructures.html#more-on-lists
Prossimi argomenti¶
- Definire classi personalizzate
- Ereditarietà e polimorfismo
- Eccezioni e gestione degli errori
- Moduli e package
import numpy as np
import matplotlib.pyplot as plt
a = np.array([1,2,3])
np.append(a,a.prod())
array([1, 2, 3, 6])
def primes(N):
primes = np.ones(N)
primes[0] = 2;
primes[1] = 3;
i = 2
while(i<N):
primes[i] = primes[:i+1].prod() + 1
i +=1
return primes
real_primes.cumprod() +1
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[37], line 1 ----> 1 real_primes.cumprod() +1 NameError: name 'real_primes' is not defined
59*509
30031
2*3*5*7*11*13+1
30031
13*139
1807
real_primes= np.array([ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29,
31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
73, 79, 83, 89, 97, 101, 103])
for x in real_primes:
print(x,1807/x)
2 903.5 3 602.3333333333334 5 361.4 7 258.14285714285717 11 164.27272727272728 13 139.0 17 106.29411764705883 19 95.10526315789474 23 78.56521739130434 29 62.310344827586206 31 58.29032258064516 37 48.83783783783784 41 44.073170731707314 43 42.02325581395349 47 38.4468085106383 53 34.094339622641506 59 30.627118644067796 61 29.62295081967213 67 26.970149253731343 71 25.450704225352112 73 24.753424657534246 79 22.873417721518987 83 21.771084337349397 89 20.303370786516854 97 18.628865979381445 101 17.89108910891089 103 17.54368932038835
real_primes= np.array([ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29,
31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
73, 79, 83, 89, 97, 101, 103])
#107 109 113
127 131 137 139 149 151 157 163 167 173
179 181 191 193 197 199 211 223 227 229
233 239 241 251 257 263 269 271 277 281
283 293 307 311 313 317 331 337 347 349
353 359 367 373 379 383 389 397 401 409
419 421 431 433 439 443 449 457 461 463
467 479 487 491 499 503 509 521 523 541
547 557 563 569 571 577 587 593 599 601
607 613 617 619 631 641 643 647 653 659
661 673 677 683 691 701 709 719 727 733
739 743 751 757 761 769 773 787 797 809
811 821 823 827 829 839 853 857 859 863
877 881 883 887 907 911 919 929 937 941
947 953 967 971 977 983 991
Cell In[43], line 6 127 131 137 139 149 151 157 163 167 173 ^ IndentationError: unexpected indent