Introduzione
Quando si inizia a usare NumPy, spesso l’attenzione va subito agli array, alle operazioni vettoriali e alla velocità rispetto alle liste Python. Tuttavia, c’è un aspetto pratico che fa davvero la differenza nello sviluppo quotidiano: capire come funzionano slicing, viste e copie.
Questo argomento è fondamentale perché in NumPy molte operazioni non creano un nuovo array completo, ma restituiscono una vista sui dati originali. Se non conosci bene questa differenza, puoi introdurre bug difficili da individuare: modifiche inattese, dati condivisi tra variabili e consumi di memoria non previsti.
In questo tutorial vedremo come:
- estrarre sotto-array con lo slicing;
- distinguere tra view e copy;
- modificare dati in sicurezza;
- evitare errori comuni quando si lavora con array multidimensionali.
È un sotto-argomento molto utile per chi usa NumPy in analisi dati, machine learning, simulazioni scientifiche o preprocessing di dataset.
Codice completo
import numpy as np
# Array di esempio: 3 righe e 4 colonne
matrice = np.array([
[10, 20, 30, 40],
[50, 60, 70, 80],
[90, 100, 110, 120]
])
print("Matrice originale:")
print(matrice)
# 1) Slicing: otteniamo una porzione dell´array
sotto_matrice = matrice[:2, 1:3]
print("nSotto-matrice ottenuta con slicing:")
print(sotto_matrice)
# 2) Modifica della vista: cambia anche l´array originale
sotto_matrice[0, 0] = 999
print("nSotto-matrice dopo la modifica:")
print(sotto_matrice)
print("nMatrice originale dopo la modifica della vista:")
print(matrice)
# 3) Creiamo una copia esplicita
copia_matrice = matrice[:2, 1:3].copy()
copia_matrice[0, 0] = 555
print("nCopia modificata:")
print(copia_matrice)
print("nMatrice originale dopo la modifica della copia:")
print(matrice)
# 4) Verifichiamo se due array condividono memoria
vista = matrice[1:, :2]
copia = matrice[1:, :2].copy()
print("nLa vista condivide memoria con la matrice originale?", np.shares_memory(vista, matrice))
print("La copia condivide memoria con la matrice originale?", np.shares_memory(copia, matrice))
# 5) Esempio pratico: normalizzare solo una colonna
dati = np.array([
[1.0, 10.0, 100.0],
[2.0, 20.0, 200.0],
[3.0, 30.0, 300.0]
])
# Prendiamo la seconda colonna come vista
colonna = dati[:, 1]
media = colonna.mean()
std = colonna.std()
# Normalizzazione z-score della colonna
dati[:, 1] = (colonna - media) / std
print("nDati dopo la normalizzazione della seconda colonna:")
print(dati) Spiegazione
Partiamo dallo slicing. In NumPy, l’espressione matrice[:2, 1:3] seleziona:
- le prime due righe, cioè gli indici 0 e 1;
- le colonne dall’indice 1 all’indice 2 incluso, perché il limite superiore nello slicing è escluso.
Il risultato non è sempre un nuovo array indipendente. Molto spesso è una vista, cioè un oggetto che punta agli stessi dati in memoria. Questo significa che se modifichi la vista, modifichi anche l’array di partenza.
Nel codice, quando eseguiamo:
sotto_matrice[0, 0] = 999 stiamo cambiando un elemento della vista. Di conseguenza, anche matrice viene aggiornata. Questo comportamento è molto utile quando vuoi lavorare in modo efficiente, ma può essere pericoloso se pensi di stare operando su una copia separata.
Per evitare effetti collaterali, usiamo .copy(). Con:
copia_matrice = matrice[:2, 1:3].copy() creiamo un nuovo array indipendente. Le modifiche su copia_matrice non influenzano più matrice.
La funzione np.shares_memory() è utile per verificare se due array condividono la stessa area di memoria. È uno strumento pratico durante il debugging, soprattutto quando il comportamento del codice sembra “strano” e sospetti che ci sia una vista nascosta.
Nell’ultimo esempio facciamo un’operazione realistica: la normalizzazione di una colonna di dati. Preleviamo la seconda colonna con dati[:, 1], calcoliamo media e deviazione standard, e poi riscriviamo i valori normalizzati direttamente nella matrice originale. Questo approccio è molto comune nel preprocessing dei dati.
Qui si vede bene il vantaggio di NumPy: puoi lavorare su colonne o righe intere con codice compatto, leggibile e veloce.
Best practice
- Usa .copy() quando vuoi isolare i dati e prevenire modifiche accidentali.
- Assumi che lo slicing produca una vista finché non hai motivo di credere il contrario.
- Controlla la condivisione della memoria con np.shares_memory() nei casi dubbi.
- Documenta chiaramente nel codice se una funzione restituisce una vista o una copia.
- Evita catene di assegnazioni ambigue quando lavori con sotto-array complessi.
- Preferisci operazioni esplicite se il codice deve essere mantenuto da altri sviluppatori.
Una regola pratica molto utile è questa: se il dato originale non deve cambiare, crea una copia esplicita. Il costo in memoria è spesso giustificato dalla maggiore sicurezza e leggibilità.
Riepilogo
In questo tutorial abbiamo visto un aspetto spesso sottovalutato di NumPy: la differenza tra slicing, viste e copie. È un concetto essenziale per scrivere codice corretto, efficiente e prevedibile.
- Lo slicing permette di estrarre porzioni di array in modo semplice.
- Molti slicing restituiscono una vista, non una copia.
- Le modifiche a una vista possono alterare l’array originale.
- Con .copy() ottieni un array indipendente.
- np.shares_memory() aiuta a verificare la relazione tra array.
Se lavori con dataset, matrici numeriche o pipeline di preprocessing, padroneggiare questo comportamento ti farà risparmiare tempo e bug difficili da tracciare.
Approfondisci con risorse ufficiali
- Documentazione ufficiale NumPy: guida generale agli array e alle operazioni fondamentali.
- NumPy User Guide: sezioni su indexing, slicing e copy/view semantics.
- Riferimento API per np.copy(), np.shares_memory() e metodi degli ndarray.
- Tutorial ufficiali NumPy per consolidare l’uso pratico degli array multidimensionali.
Se vuoi continuare, il passo successivo ideale è studiare l’indicizzazione avanzata e confrontarla con lo slicing tradizionale: lì emergono differenze importanti tra selezione per intervalli e selezione per indici specifici.
