Array: Liste e Tuple#
Discussi approfonditamente i tipi di dato principale, possiamo passare a descrivere come si dichiarano e manipolano gli array
, ovvero delle variabili che contengono al loro interno più variabili, che possono essere anche di tipo differente.
In Python esistono due tipologie principali di array: le liste
e le tuple
. La differenza principale tra liste e tuple è che le prime sono mutabili (ovvero è sempre possibile accedere gli elementi al loro interno e modificarli), mentre le seconde sono immutabili (una volta che un elemento è stato dichiarato, non è più possibile modificarlo). Vedremo meglio questa differenza proseguendo con la discussione.
A differenza di altri linguaggi di programmazione, dove è necessario inizializzare gli array definendone in anticipo la lunghezza, su Python gli array sono dinamici, ovvero possono essere allungati/accorciati al bisogno.
Per definire una lista, si inseriscono semplicemente una serie di valori, racchiusi all’interno di due parentesi quadre []
.
a = [1, 5, 3] # Definiamo una lista di numeri
b = ["c", "i", "a", "o"] # Definiamo una lista di stringhe
c = ["c", 3, True] # Definiamo una lista mista
print(type(a))
print(a)
print(b)
print(c)
<class 'list'>
[1, 5, 3]
['c', 'i', 'a', 'o']
['c', 3, True]
Similmente, per definire una tupla, è sufficiente inserire i valori all’interno di due parentesi tonde ()
, con valori separati da una virgola.
Note
Una curiosità: è in realtà la presenza di una virgola ,
che separa gli oggetti a dare luogo ad una tupla, non le parentesi tonde in sè. Infatti, è in molti casi (non sempre) possibile rimuovere le parentesi tonde senza che questo dia luogo ad un’errore.
t = (3, "pippo", [1, 3, 2]) # Tuple e liste possono contenere al loro interno anche altri array
print(type(t))
print(t)
<class 'tuple'>
(3, 'pippo', [1, 3, 2])
Note
La possibilità di inserire liste all’interno di liste è particolarmente importante per l’utilizzo di Python per l’algebra lineare. Infatti, se i vettori sono definiti come delle liste, allora è possibile definire delle matrici considerando una lista di liste, dove ciascuna lista interna rappresenta una riga della matrice, e così via.
Per tipizzare una lista o una tupla in fase di dichiarazione, si inserisce il tipo di array, seguito da una parentesi graffa []
, con scritto il tipo di dato che ci aspettiamo verrà inserito al suo interno.
a: list[int] = [2, 1, 3] # Crea una list di interi, tipizzata
Slicing#
Una delle operazioni più comune quando si lavora su array è lo slicing. Questo consiste nell’estrarre un dato elemento dell’array. Per farlo, è sufficiente far seguire l’array di cui si desidera estrarre un’elemento da una parentesi quadra []
, scrivendo la posizione dell’elemento che si vuole estrarre.
Note
In Python, la posizione degli elementi in un’array inizia dall’elemento di posto zero. Quindi, su un’ipotetica lista l = [2, 4, 1]
, il primo elemento (il 2), corrisponde alla posizione zero, il secondo corrisponde alla posizione uno e così via.
# Crea una lista
l = [2, 1, 5]
# Estri un suo elemento
print(f"L'elemento di posto 2 nella lista è: {l[2]}.")
L'elemento di posto 2 nella lista è: 5.
E’ similmente possibile anche utilizzare lo slicing per estrarre da un’array non un singolo elemento, ma un sotto-blocco di elementi. Questo viene fatto inserendo all’interno delle parentesi quadre gli indici desiderati, con la sintassi inizio:fine:step
.
Note
L’indice di inizio
è incluso nella selezione, mentre l’indice di fine
è escluso.
Ad esempio:
a: list[int] = [1, -1, 0, 1, 3, -2, 1] # Creo una lista di valori
# Estraggo gli elementi di indice pari
pari = a[0:7:2] # Estraggo gli elementi di indice da 0 (primo elemento) a 6 (fino a indice 7 escluso), con passo 2.
print(pari)
# Estraggo i 3 elementi centrali
centrali = a[2:5]
print(centrali)
[1, 0, 3, 1]
[0, 1, 3]
E’ possibile far capire a Python che vogliamo estrarre l’ultimo elemento di un’array, in vari modi equivalenti, che possono essere usati a preferenza.
Un primo metodo è quello di utilizzare la funzione
len()
che, applicata ad un’array, ritorna un valore intero che ne misura la lunghezza. L’ultimo elemento sarà quindi quello in posizionelen(l) - 1
.Un’altro metodo è quello di utilizzare indici negativi. E’ infatti possibile richiamare con l’operatore di slicing anche indici negativi. Quello che farà Python sarà accedere all’array contando al contrario, dalla fine verso l’inizio. L’ultimo elemento sarà quindi quello con indice
-1
.Infine, il metodo più semplice (ma non il più chiaro) di accedere all’ultimo elemento di un’array (o similmente al primo), è quello di lasciare semplicemente vuoto il rispettivo campo nello slicing. Infatti, una scrittura del tipo
2::2
, indica che vogliamo estrarre dall’array il sotto-array ottenuto prendendo gli elementi dal posto 2, fino alla fine (poiché il valore difine
non è inserito), con uno step pari a 2.
# Definisco un'array
a: list[int] = [1, -1, 0, 1, 3, -2, 1]
print(f"Il numero di elementi di a è: {len(a)}.")
# Estraggo gli elementi dal posto 2 fino alla fine
print(a[2:len(a)]) # Da 2 fino len(a) - 1.
print(a[2:-1]) # Da 2 fino a -1.
print(a[2:]) # Da 2 fino in fondo
Il numero di elementi di a è: 7.
[0, 1, 3, -2, 1]
[0, 1, 3, -2]
[0, 1, 3, -2, 1]
Utilizzando lo slicing è possibile anche invertire un’array, visualizzandola al contrario. Per fare ciò è sufficiente considerare uno slicing che parte dalla fine, arriva all’inizio dell’array, con step = -1
.
# Definisco un'array
a: tuple[int] = (1, 2, 3)
print(a[::-1]) # Array invertito
(3, 2, 1)
Liste#
Un grande vantaggio delle liste è il loro essere modificabili. Questo significa che è possibile accedere (tramite lo slicing) ad un suo elemento, e cambiarne il valore in fase successiva alla creazione della variabile.
# Creazione di una lista
a = [1, 2, 3]
print(a)
# Modificare un elemento
a[1] = -1
print(a)
[1, 2, 3]
[1, -1, 3]
O, utilizzando il metodo .append(valore)
, è possibile inserire un’elemento in fondo alla lista, aumentandone la lunghezza di 1, oppure inserire un’elemento addizionale in una posizione specifica tramite il metodo .insert(posizione, valore)
.
# Creo una lista
a = [1, 2, 3]
print(a)
# Aggiungo un valore in fondo
a.append(4)
print(a)
# Aggiungo un valore in posto 1
a.insert(1, 10)
print(a)
[1, 2, 3]
[1, 2, 3, 4]
[1, 10, 2, 3, 4]
E’ infine possibile concatenare due liste utilizzando l’operatore +
. Infatti, date due liste a
e b
, definita c = a + b
, si ha che c
sarà una lista che contiene tutti gli elementi di a
e tutti gli elementi di b
, in successione.
# Definisco due liste
a = ["Ciao", "a"]
b = ["tutti", "!"]
# Concateno le due liste
c = a + b
# Visualizzo il risultato
print(c)
['Ciao', 'a', 'tutti', '!']
Tuple#
Le tuple rappresentano in Python la versione di array alternativa alle liste. Come già detto, vengono definite mediante un’elenco di valori, separati da una virgola ,
e spesso circoscritti da delle parentesi tonde ()
. Le parentesi tonde non sono però fondamentali, poiché la vera caratterizzazione delle tuple sono proprio le virgole che ne separano gli oggetti, non le parentesi.
# Definisco una tupla "classica"
a = (1, 3, 2)
print(f"a = {a}.")
# E una versione senza parentesi tonde
b = 1, 3, 2
print(f"b = {b}.")
a = (1, 3, 2).
b = (1, 3, 2).
Warning
Per lo stesso motivo, dichiarare una tupla con un solo elemento all’interno, non può essere fatto scrivendo un valore circoscritto da parentesi tonde, come ad esempio a = (3)
, poiché non essendoci le virgole, questo oggetto non verrà riconosciuto come tupla.
Per ottenere una tupla con un singolo elemento, è necessario scrivere espressioni del tipo: a = (3,)
.
# Costruisco una tupla con un singolo elemento (modo corretto)
a = (3,)
print(type(a))
# Costruisco una tupla con un singolo elemento (modo sbagliato)
b = (3)
print(type(b))
<class 'tuple'>
<class 'int'>
Come già detto, una volta definita una tuple, provare a modificarne un’elemento restituisce errore.
# Creo una tupla
a: tuple[int] = (1, 3, 2)
# Modifico il suo elemento di posto 1
a[1] = 1
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[13], line 5
2 a: tuple[int] = (1, 3, 2)
4 # Modifico il suo elemento di posto 1
----> 5 a[1] = 1
TypeError: 'tuple' object does not support item assignment
Per tutte le altre situazioni, le tuple si comportano in maniera molto simile alle liste. Ad esempio, si possono concatenare con l’operatore +
.
# Creo due tuple
a: tuple[int] = (1, 3, 2)
b: tuple[int] = (9, 5, 8)
# Concateno le tuple
c = a + b
print(c)
(1, 3, 2, 9, 5, 8)
Ci chiediamo quindi: perché utilizzare le tuple rispetto alle liste, e in quali situazioni è conveniente?
Prima di tutto, un questione di memoria: data la loro immutabilità, le tuple sono salvate in memoria in modo molto più efficiente rispetto alle liste, e questo le rende migliori dal punto di vista dell’efficienza.
Inoltre, a livello sintattico, la loro forza risiede nell’assegnazione multipla. Infatti, è possibile distribuire i valori di una tupla a più variabili semplicemente utilizzando le tuple. Ad esempio:
# Creo una tupla
risultato = (5, "Rosso")
# Assegno il valore della tupla a delle variabili
posizione, colore = risultato
print(posizione)
print(colore)
5
Rosso
Questo è particolarmente comodo in varie situazioni, come vedremo più avanti nel corso.
Approfondimento 1: Puntatori#
Andiamo qui a discutere alcuni casi limiti in cui si potrebbe incorrere utilizzando array in Python, ed in particolare le liste. Il problema è il seguente: supponiamo di definire una variabile a
, che rappresenta una lista. Al momento della dichiarazione, verranno caricate in memoria tutte le variabili necessarie, e la lista risultante verrà associata all’elemento a
.
Ora, supponiamo di definire una seconda variabile b = a
. Tramite questa istruzione, Python non andrà a creare una nuova lista b
, dentro cui caricherà una copia degli elementi di a
, ma semplicemente definirà la variabile b
come un’oggetto che punta alla stessa cella di memoria in cui è contenuto a
. Per questo motivo, andando a modificare a
, anche la variabile b
verrà modificata.
# Definisco una lista
a: list[str] = ["Mi", "chiamo", "Luca"]
print(a)
# Assegno una nuova variabile allo stesso oggetto
b = a
print(b)
# Modifico a
a[-1] = "Davide"
print(a)
print(b)
# O anche
a.append("Evangelista")
print(b)
['Mi', 'chiamo', 'Luca']
['Mi', 'chiamo', 'Luca']
['Mi', 'chiamo', 'Davide']
['Mi', 'chiamo', 'Davide']
['Mi', 'chiamo', 'Davide', 'Evangelista']
Per evitare che questo accada (e quindi assegnare a b
una copia dell’oggetto a cui è associato a
), è necessario utilizzare un semplice work-around:
# Definisco una lista
a: list[str] = ["Mi", "chiamo", "Luca"]
print(a)
# Assegno una nuova variabile allo stesso oggetto
b = a[:] # Attenzione ai [:].
print(b)
# Modifico a
a[-1] = "Davide"
print(a)
print(b)
['Mi', 'chiamo', 'Luca']
['Mi', 'chiamo', 'Luca']
['Mi', 'chiamo', 'Davide']
['Mi', 'chiamo', 'Luca']
Chiaramente, il “problema” del puntare allo stesso oggetto non si verifica nel caso in cui la nuova variabile si ottiene tramite operazioni sulla vecchia variabile:
# Definisco una lista
a: list[str] = ["Mi", "chiamo", "Luca"]
print(a)
# Assegno una nuova variabile
b = a + ["Evangelista"]
# Modifico a
a[-1] = "Davide"
print(a)
print(b)
['Mi', 'chiamo', 'Luca']
['Mi', 'chiamo', 'Davide']
['Mi', 'chiamo', 'Luca', 'Evangelista']
Approfondimento 2: Inizializzazione#
La possibilità di aggiungere elementi in una lista, non viene senza conseguenze. Infatti, utilizzando il metodo .append()
per aggiungere elementi ad una lista, esegue la seguente serie di steps:
Viene creata una nuova lista, copia della prima, con lunghezza 1 in più della lista di partenza.
L’ultimo elemento della nuova lista corrisponde all’elemento indicato col metodo
.append()
.La lista iniziale viene eliminata (per opera del garbage collector).
Come risultato, lo spazio in memoria richiesto per allungare una lista data di un elemento è doppio rispetto alla memoria occupata dalla lista stessa. Questo può diventare proibitivo in alcune applicazioni, dove la lista viene allungata varie volte nel giro di un singolo programma.
Per questo motivo, quando ci si aspetta a priori di dover allungare una lista, è buona pratica inizializzare una lista vuota (utilizzando un valore di default, tipo lo 0), i cui elementi verranno sostituiti durante l’esecuzione del programma. Questa fase, chiamata inizializzazione, permette di rendere il codice molto più efficiente in alcuni scenari, come ad esempio quando la lista viene utilizzata per memorizzare alcune informazioni durante le iterazioni di un algoritmo (tipo l’andamento di un’errore).
# Inizializzo una lista vuota (lunga 4)
a: list[int] = [0] * 4
# Inserisco gli elementi al suo interno
a[0] = 1
a[1] = 2
a[2] = 3
a[3] = 4
print(a)
[1, 2, 3, 4]