Nella lezione precedente, abbiamo visto e analizzato la classe dtype
, i principali tipi di dati e come definirli per poter lavorare al meglio con questa libreria e per fare conversioni di dati e calcoli matematici in modo semplice ed efficace.
In questa lezione, invece, ci concentreremo sulla principale struttura dati alla base di questo framework: gli n-dimensional array.
Gli n-dimensional array
Un n-dimensional array
(ndarry
) è un tipo di struttura dati che ha reso Numpy la libreria Python perfetta per il calcolo matematico, ottimizzando le performance in termini di costo computazionale e velocità.
Volendo utilizzare la definizione offerta dalla documentazione ufficiale, un ndarry
rappresenta un array multidimensionale e omogeneo di elementi di dimensioni fisse. Un tipo di dato associato all’oggetto array descrive il formato di ogni elemento presente in esso, tra cui:
- il suo ordine di byte;
- quanti byte occupa in memoria;
- se è un numero intero, un numero in virgola mobile o altro.
Possiamo definire più semplicemente un ndarray
come un contenitore multidimensionale di elementi dello stesso tipo e dimensione. In particolare, il numero di dimensioni ed elementi in un array è definito dal suo attributo shape, che è rappresentato tramite una tupla di N
numeri interi e non negativi, che specificano le dimensioni di ciascuna dimensione.
Il tipo di elementi di questa particolare struttura dati è specificato da un dtype associato all’intero ndarray
. Vediamo un semplice esempio.
Per creare un ndarry bidimensionale, ossia una matrice, composto da 3 righe e 3 colonne di float, basterà utilizzare il metodo array
del modulo numpy
.
import numpy as np
matrix = np.array([[1.2, 2.4, 3.1], [4.5, 5.2, 6.9], [7.2, 8.8, .9]], np.float32)
La variabile matrix
è quindi un ndarray
, come facilmente ricavabile dall’utilizzo della funzione type
.
type(matrix)
numpy.ndarray
A questo punto, per conoscerne la dimensione basterà utilizzare l’attributo shape
come segue.
matrix.shape
(3, 3)
Invece, per verificarne il tipo, basterà usare l’attributo dtype
.
matrix.dtype
dtype('float32')
Infine, se vogliamo conoscere il numero di elementi e il numero di dimensioni che compongono l’oggetto matrix
, possiamo usare l’attributo size
e ndim
, rispettivamente, ottenendo.
print(f'matrix dimension: {matrix.ndim} - total elements: {matrix.size}')
matrix dimension: 2 - total elements: 9
Quelli visti fin qui sono solo alcuni degli attributi offerti dall’oggetto ndarray
, ve ne sono altri come imag
o real
per ottenere la parte immaginaria o reale dell’array. Per maggiori informazioni, si rimanda alla documentazione ufficiale.
Come con altre strutture dati offerte da Python, con gli ndarray
è possibile:
- accedere al contenuto di un
ndarray
; - modificare il contenuto di un
ndarray
tramite l’operazione di indexing; - modificare un
ndarray
tramite l’operazione slicing o tramite i metodi e gli attributi offerti dalla classe stessa.
Ad esempio, per accedere ad uno specifico elemento della matrice, basterà indicare l’indice per la riga e per la colonna della posizione a cui si vuole accedere.
matrix[1,2]
6.9
Nelle prossime lezioni, vedremo nelle specifico le diverse operazioni che possono essere compiute sugli ndarray
.
Gestione della memoria interna di un ndarray
A un'istanza della classe ndarray
viene assegnato un segmento continuo nella memoria. In particolare, si ha che:
- l'allocazione della memoria insieme allo schema di indicizzazione mappa quindi N-interi su un elemento del blocco dell'array;
- il range di variazione dell'indice è dato dalla forma dell'array e viceversa;
- il tipo di dato viene utilizzato per definire quanti byte richiederà ogni elemento dell’
ndarray
; - il tipo di dato permette di definire come verranno inferiti i byte. Ad esempio, ogni elemento
int16
ha una dimensione di 16 bit, cioè16/8 = 2 byte
.
Infine, un altro aspetto interessante degli ndarray
è che diversi ndarray
possono condividere gli stessi dati, in modo che le modifiche apportate in un ndarray
possano essere visibili in un altro. In questo caso, si parla di view
e di base
, dove:
- un
base ndarray
è l’oggetto che definisce i dati; - un
view ndarray
è unndarray
creato a partire dal basendarray
con cui condivide le celle di memoria.
Una qualsiasi modifica fatta su un base
o un view ndarray
verrà vista sull’altro.
Per creare una view, è possibile usare il metodo view()
di ndarray
e per sapere se effettivamente due ndarray
condividono la memoria, è possibile usare il metodo shares_memory()
del modulo numpy
. Vediamo un semplice esempio:
base = np.array([1, 2, 3, 4, 5])
view = base.view()
view[0] = 42
print(f'base: {base}')
print(f'view: {view}')
print(f'shared memory: {np.shares_memory(base, view)}')
# results
base: [42 2 3 4 5]
view: [42 2 3 4 5]
shared memory: True
ndarray vs list
Erroneamente, si tende a pensare che gli ndarray
siano simili alle list
in Python, ma le differenze sono notevoli. In Python un oggetto list
:
- è definito inserendo uno o più elementi tra parentesi quadre;
- è ordinato mostrando i dati in un ordine specifico;
- è mutabile;
- non deve essere dichiarato;
- non può gestire direttamente le operazioni matematiche.
Contrariamente gli ndarray
:
- devono essere dichiarati
- vengono creati tramite l’apposita funzione
array()
del modulonumpy
; - possono definire un solo tipo di dato;
- possono memorizzare i dati in modo compatto, risultando efficienti per l’archiviazione di grandi quantità di dati;
- sono ottimi per le operazioni matematiche.
Ad esempio, si può dividere ogni elemento di un array per lo stesso numero con una sola riga di codice.
array = np.array([3, 6, 9, 12])
division = array/3
print(division)
[1. 2. 3. 4.]
print (type(division))
<class 'numpy.ndarray'>
Se provassimo a fare lo stesso con una list, otterremmo un errore.
list = [3, 6, 9, 12]
division = list/3
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-18-f127235414b2> in <module>()
1 list = [3, 6, 9, 12]
----> 2 division = list/3
TypeError: unsupported operand type(s) for /: 'list' and 'int'