Introduzione alle Ufunc NumPy

Quando si inizia a usare NumPy, uno dei concetti più importanti da comprendere è quello delle ufunc. Questo termine compare spesso nella documentazione ufficiale e in molte guide pratiche, perché rappresenta uno dei pilastri del lavoro con gli array in Python. Capire bene come funzionano le ufunc significa scrivere codice più pulito, più rapido e spesso anche più leggibile.

In questa guida firmata Codegrind vedremo in modo semplice che cosa sono le ufunc di NumPy, perché vengono considerate così centrali e in che modo si collegano al concetto di vectorization. L’obiettivo è offrire un’introduzione chiara anche a chi è alle prime armi, senza rinunciare alla precisione tecnica necessaria per costruire basi solide.

Cosa sono le ufunc in NumPy e come funzionano

Il termine ufunc è l’abbreviazione di universal function. In NumPy, una ufunc è una funzione progettata per eseguire un’operazione su uno o più elementi di un array in modo efficiente. In pratica, invece di scorrere i valori uno per uno con un ciclo scritto a mano, si può applicare direttamente una funzione all’intero array.

Per esempio, operazioni molto comuni come la somma, la sottrazione, il valore assoluto, il seno, il coseno o l’elevamento a potenza vengono gestite da ufunc ottimizzate. Questo permette di lavorare con grandi quantità di dati con un approccio molto più naturale rispetto alle liste Python tradizionali.

Un primo esempio aiuta a chiarire subito l’idea.

import numpy as np

numeri = np.array([1, 2, 3, 4])

risultato = np.sqrt(numeri)

print(risultato)

In questo caso, np.sqrt è una ufunc. Non si limita a lavorare su un singolo numero: prende l’intero array e calcola la radice quadrata di ciascun elemento. Il risultato è un nuovo array con i valori trasformati.

Le ufunc possono essere di due tipi molto comuni. Le unary ufunc lavorano su un solo input, come np.sqrt, np.exp o np.sin. Le binary ufunc, invece, lavorano su due input, come np.add, np.multiply o np.maximum.

import numpy as np

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

somma = np.add(a, b)
prodotto = np.multiply(a, b)

print(somma)
print(prodotto)

Naturalmente, in NumPy molte di queste operazioni si possono scrivere anche con gli operatori classici come + e *, ma dietro le quinte entra spesso in gioco proprio il meccanismo delle ufunc.

Un aspetto molto interessante è che le ufunc supportano il broadcasting, cioè la capacità di applicare operazioni tra array con forme compatibili senza dover replicare manualmente i dati. È una caratteristica potente, anche se all’inizio conviene assimilarla con calma.

import numpy as np

a = np.array([1, 2, 3])
b = 10

risultato = np.add(a, b)

print(risultato)

Qui NumPy aggiunge il valore 10 a ogni elemento dell’array, senza bisogno di un ciclo esplicito. Questo stile di lavoro è proprio uno dei motivi per cui NumPy viene utilizzato così spesso in ambito scientifico, analitico e nel machine learning.

Perché conviene usare le ufunc di NumPy nei progetti Python

Usare le ufunc non è soltanto una questione di comodità. Il loro vero vantaggio emerge quando si lavora con array numerici di dimensioni medio-grandi. In questi casi, affidarsi a funzioni ottimizzate di NumPy è in genere molto più efficiente rispetto a scrivere cicli for in Python puro.

Il primo beneficio è la velocità. Le ufunc sono implementate in modo da sfruttare routine interne altamente ottimizzate. Per questo motivo riescono a elaborare molti dati in tempi ridotti.

Il secondo beneficio è la leggibilità. Un’operazione espressa con una ufunc risulta spesso più immediata da comprendere rispetto a un blocco di codice con accumuli, condizioni e iterazioni manuali.

Il terzo beneficio è la riduzione degli errori. Meno codice ripetitivo significa anche meno occasioni per introdurre bug banali, soprattutto quando si sta imparando.

Proviamo a confrontare un approccio classico con uno basato su NumPy. Supponiamo di voler calcolare il quadrato di una serie di valori.

Con un ciclo Python si potrebbe scrivere così:

numeri = [1, 2, 3, 4]
quadrati = []

for numero in numeri
{
    quadrati.append(numero ** 2)
}

print(quadrati)

Con NumPy, invece, l’operazione diventa molto più diretta:

import numpy as np

numeri = np.array([1, 2, 3, 4])

quadrati = np.square(numeri)

print(quadrati)

Il secondo esempio è più compatto, più espressivo e rispecchia meglio il modo in cui si lavora con strutture dati numeriche. Inoltre, in contesti reali con array molto più grandi, la differenza di prestazioni può essere significativa.

Un altro punto forte riguarda la coerenza. Una volta compreso il comportamento delle ufunc, molte operazioni NumPy iniziano a sembrare parte di un unico linguaggio operativo. Questo rende più semplice leggere la documentazione ufficiale e passare da un’operazione all’altra senza sentirsi disorientati.

Vectorization con NumPy: perché è un concetto chiave

Parlando di ufunc, prima o poi si incontra inevitabilmente il termine vectorization. In NumPy, la vectorization consiste nello scrivere operazioni che agiscono direttamente sugli array, evitando per quanto possibile i cicli espliciti in Python.

Non significa soltanto “fare le stesse cose più in fretta”. Significa adottare un modo di pensare più adatto al calcolo numerico: invece di ragionare elemento per elemento, si ragiona su blocchi di dati.

Facciamo un esempio molto semplice. Immaginiamo di voler sommare due insiemi di numeri corrispondenti.

import numpy as np

a = np.array([10, 20, 30])
b = np.array([1, 2, 3])

risultato = a + b

print(risultato)

Questa è vectorization: un’unica istruzione produce l’operazione su tutti gli elementi compatibili. Non c’è bisogno di scrivere un ciclo, né di gestire manualmente gli indici. Il codice resta corto, leggibile e molto vicino al significato matematico dell’operazione.

La vectorization si appoggia proprio alle ufunc. Quando si scrive un’espressione come a + b, NumPy utilizza internamente una funzione universale per elaborare i dati. Per questo motivo i due concetti sono strettamente collegati.

Un altro esempio utile è l’applicazione di una condizione a un intero array.

import numpy as np

valori = np.array([5, 12, 7, 18, 3])

maschera = valori > 10

print(maschera)

Anche il confronto valori > 10 viene gestito in modo vettoriale. Il risultato non è un singolo valore booleano, ma un array di True e False che descrive la verifica elemento per elemento. Questo meccanismo è alla base di molte tecniche di filtraggio e analisi dati in NumPy.

Per chi inizia, il consiglio migliore è abituarsi presto a questo approccio. Se viene spontaneo scrivere un ciclo, vale la pena chiedersi: “Posso farlo direttamente con una ufunc o con un’operazione vettoriale?”. Spesso la risposta è sì, ed è proprio lì che NumPy dà il meglio di sé.

Esempi pratici per capire meglio le ufunc e la vectorization

Vediamo ora alcuni piccoli esempi concreti, utili per fissare i concetti principali.

Calcolare il valore assoluto

import numpy as np

valori = np.array([-3, -1, 0, 2, 4])

assoluti = np.abs(valori)

print(assoluti)

Sommare una costante a tutti gli elementi

import numpy as np

valori = np.array([1, 2, 3])

nuovi_valori = valori + 5

print(nuovi_valori)

Combinare più operazioni in modo compatto

import numpy as np

valori = np.array([1, 4, 9, 16])

risultato = np.sqrt(valori) + 2

print(risultato)

Questi esempi mostrano bene una delle qualità più apprezzate di NumPy: la possibilità di trasformare, confrontare e combinare dati numerici con sintassi chiara e risultati immediati.

Errori comuni da evitare quando si iniziano a usare le ufunc

Chi muove i primi passi con NumPy incontra spesso alcune difficoltà ricorrenti. La prima è confondere le liste Python con gli array NumPy. Le ufunc lavorano al meglio con gli array NumPy, mentre le liste seguono logiche diverse.

Per esempio, questa operazione con una lista non produce una somma elemento per elemento:

lista = [1, 2, 3]

print(lista * 2)

Con un array NumPy, invece, il comportamento è numerico:

import numpy as np

array = np.array([1, 2, 3])

print(array * 2)

Un altro errore comune è ignorare la shape degli array. Se si tenta di combinare strutture non compatibili, NumPy può restituire un errore legato al broadcasting. È quindi utile controllare sempre forma e dimensioni dei dati quando un’operazione non si comporta come previsto.

Infine, c’è la tendenza a usare cicli Python anche quando non servono. È una fase normale dell’apprendimento, ma con un po’ di pratica si impara a riconoscere le situazioni in cui una ufunc rende il codice più efficace e più elegante.

Corsi correlati

Design Pattern per videogiochi

I Design Pattern sono soluzioni a problemi comuni che si incontrano nello sviluppo del software. Nel contesto dei videogiochi, questi pattern possono aiutare a gestire la complessità crescente dei giochi moderni. Sei appassionato di videogiochi? Hai mai pensato a come vengono progettati e sviluppati? Sei curioso di scoprire i segreti dietro la creazione dei tuoi giochi preferiti? Allora sei nel posto giusto! Questo corso è stato progettato per fornirti una solida comprensione dei Design Pattern, strumenti fondamentali per ogni sviluppatore di giochi. Attraverso esempi pratici e realistici, esploreremo insieme come questi pattern possono aiutarti a creare giochi più efficienti, flessibili e mantenibili.

Corso Fondamenti di Python

Python, uno dei linguaggi di programmazione più popolari al mondo, è ampiamente utilizzato da colossi dell’informatica e della tecnologia come Google, la Nasa e Mozilla.
Questo corso sui fondamenti di Python esplorerà le ragioni del suo successo duraturo. Impareremo la potente ed espressiva sintassi di Python, che consente di ottenere risultati impressionanti con poche righe di codice.

Blender Masterclass 2

I paesaggi, in generale, possono risultare intimidatori data la loro complessità, costituita da numerosi elementi che sembrano formare qualcosa di virtualmente infinito. Durante le lezioni di questo corso, ci concentreremo su una scomposizione delle diverse fasi coinvolte nella creazione di una scena, affrontandole una alla volta attraverso passi graduati.

Blender Masterclass 1

In questo corso di Blender 3D, Masha, in collaborazione con Gedemy, vi mostrerà il processo di modellazione, UV mapping, shading, e texture painting di un asset. Una volta apprese le basi, vedremo anche come effettuare il baking delle textures e la preparazione con successivo export della mesh in modo che possa essere utilizzabile all’interno di un qualsiasi altro software.

Crea il tuo account gratuito Gedemy

Gedemy è la prima piattaforma di formazione italiana che ti permette di sviluppare competenze in ambiti come Game Design, Programming, Arte, 3D, Tech, AI e molto altro.