Dialogo con Gemini.
Qual è il principale linguaggio di programmazione usato in ambito AI e perchè?
Il principale linguaggio di programmazione utilizzato nell’ambito dell’Intelligenza Artificiale e del Machine Learning è Python.
La sua egemonia non è casuale, ma è il risultato di un preciso mix di fattori tecnici, storici ed ecosistemici:
1. Sintassi semplice e leggibile Aulab
Python è progettato per essere chiaro e conciso. Questo permette a ricercatori, matematici e data scientist di concentrarsi sulla progettazione di modelli e algoritmi complessi senza perdersi nei dettagli di basso livello della gestione della memoria o della sintassi pesante.
2. Un ecosistema di librerie ineguagliabile
Python dispone di librerie e framework open-source che costituiscono lo standard de facto dell’AI:
- Machine Learning & Deep Learning: PyTorch, TensorFlow, Keras, Scikit-learn.
- Calcolo scientifico e manipolazione dati: NumPy, Pandas, SciPy.
- Generative AI e NLP: Hugging Face Transformers, LangChain, NLTK.
3. Architettura “Glue Language” (Ibrida)
Anche se Python è un linguaggio interpretato (e quindi intrinsecamente più lento di linguaggi compilati come C++ o Rust), le librerie AI più critiche (es. PyTorch o TensorFlow) hanno il loro “core” di calcolo ottimizzato e scritto in C/C++ e CUDA per l’elaborazione su GPU/TPU. Python agisce da interfaccia ad alto livello: gli sviluppatori scrivono codice leggibile in Python, mentre i calcoli tensoriali intensivi vengono eseguiti ad altissima velocità dall’hardware sottostante.
4. Prototipazione rapida e sviluppo iterativo
L’esplorazione dei dati e l’addestramento dei modelli richiedono continue prove ed errori. L’uso di strumenti come i Jupyter Notebook (basati su Python) consente di eseguire parti di codice in tempo reale, visualizzare grafici e analizzare i risultati istantaneamente, velocizzando il processo rispetto al ciclo di compilazione tradizionale.
5. Community e standard di settore
L’adozione da parte delle principali aziende tech (Google, Meta, OpenAI) e del mondo accademico ha generato un circolo virtuoso: quasi tutte le nuove ricerche e i nuovi modelli generativi vengono rilasciati prima di tutto con API e repository in Python.
Altri linguaggi rilevanti nel settore
Sebbene Python domini la fase di ricerca e prototipazione, altri linguaggi ricoprono ruoli specifici:
- C++ / Rust: Usati per il deployment ad alte prestazioni, nei sistemi embedded o per la scrittura dei motori di inferenza dove la latenza deve essere minima. Culture Digitali srl
- SQL / R: Molto diffusi nell’estrazione dati (SQL) e nell’analisi statistica/esplorativa (R).
- Mojo: Un linguaggio emergente nato proprio con l’obiettivo di combinare la sintassi di Python con le prestazioni del C++.
Potresti scrivere degli esempi che confrontino Mojo con Python?
Ecco alcuni esempi pratici che mostrano dove Mojo si distingue da Python, mantenendo una sintassi familiare ma introducendo caratteristiche fondamentali dei linguaggi ad alte prestazioni (tipo di dato statico, gestione diretta della memoria e parallelismo).
1. Dichiarazione delle Variabili e Tipi (def vs fn)
In Python le variabili sono dinamiche. In Mojo puoi usare la classica sintassi Python (def), ma per ottenere la massima velocità usi fn, che impone il controllo dei tipi al momento della compilazione e non consente variabili non dichiarate.
Python:
Python
def saluta(nome: str) -> str:
messaggio = "Ciao, " + nome
return messaggio
print(saluta("Pierluigi"))
Mojo:
Snippet di codice
# 'fn' attiva il controllo rigoroso dei tipi e l'assenza di overhead a runtime
fn saluta(nome: String) -> String:
# 'let' dichiara una costante immutabile
# 'var' dichiarerebbe una variabile mutabile
let messaggio: String = "Ciao, " + nome
return messaggio
fn main():
print(saluta("Pierluigi"))
2. Strutture dati a basso livello (Struct vs Class)
In Python le classi hanno costi di overhead per la gestione dinamica degli attributi. Mojo introduce le struct: strutture allocate direttamente in memoria (sullo stack invece che nell’heap), senza l’overhead di una classe Python standard.
Python:
Python
class Punto:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
p = Punto(1.5, 2.5)
print(p.x, p.y)
Mojo:
Snippet di codice
struct Punto:
var x: Float64
var y: Float64
# Metodo costruttore
fn __init__(out self, x: Float64, y: Float64):
self.x = x
self.y = y
fn main():
let p = Punto(1.5, 2.5)
print(p.x, p.y)
3. Vettorizzazione SIMD e Ottimizzazione Hardware
In Python, se vuoi eseguire operazioni vettoriali veloci (SIMD – Single Instruction, Multiple Data), devi fare affidamento a librerie esterne scritte in C come NumPy. In Mojo, la vettorizzazione è integrata nativamente nel linguaggio tramite il tipo SIMD.
Python (con NumPy):
Python
import numpy as np
# Creazione di due vettori float32
a = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float32)
b = np.array([5.0, 6.0, 7.0, 8.0], dtype=np.float32)
c = a + b
print(c)
Mojo (Nativo):
Snippet di codice
from sys.info import simdwidthof
fn main():
# Definisce un registro SIMD da 4 elementi Float32
alias size = 4
let a = SIMD[DType.float32, size](1.0, 2.0, 3.0, 4.0)
let b = SIMD[DType.float32, size](5.0, 6.0, 7.0, 8.0)
# L'addizione viene eseguita in un singolo ciclo di clock della CPU
let c = a + b
print(c)
4. Interoperabilità Diretta: Chiamare codice Python da Mojo
Mojo non richiede di riscrivere tutto da zero. Puoi importare qualsiasi modulo o libreria Python esistente (come numpy, matplotlib o torch) direttamente dentro un file Mojo.
Mojo:
Snippet di codice
from python import Python
fn main() raises:
# Importa il modulo Python standard 'math' e la libreria 'numpy'
let np = Python.import_module("numpy")
# Crea un array NumPy direttamente in Mojo
let arr = np.array([1, 2, 3, 4, 5])
print(arr.mean())
Sintesi delle differenze chiave
| Caratteristica | Python | Mojo |
|---|---|---|
| Esecuzione | Interpretato (Bytecode / CPython) | Compilato direttamente in codice macchina (tramite MLIR/LLVM) |
| Tipizzazione | Dinamica (i type hints sono solo decorativi) | Statica e forte con fn (opzionalmente dinamica con def) |
| Gestione Memoria | Garbage Collector / Reference Counting | Ownership/Borrowing e allocazione manuale/automatica senza GC |
| Prestazioni | Moderate (richiede C/C++ per i calcoli pesanti) | Paragonabili a C/C++ e Rust |
Vuoi approfondire l’uso delle struct e del sistema di ownership in Mojo?Sì
Possiamo di dire che la programmazione è esclusivamente ad oggetti in entrambi ambienti?
No, nessuno dei due ambienti è esclusivamente ad oggetti. Sia Python che Mojo sono linguaggi multiparadigma, progettati per permettere di combinare programmazione orientata agli oggetti (OOP), procedurale e funzionale.
1. Python: “Tutto è un oggetto”, ma il paradigma non è solo OOP
In Python esiste una distinzione fondamentale tra l’architettura interna del linguaggio e lo stile di programmazione che puoi adottare:
- Architettura interna: In Python tutto è un oggetto dal punto di vista dell’interprete (i numeri interi, le funzioni, le stringhe, i moduli, perfino le classi stesse). Ognuno di essi ha un ID, un tipo e dei metodi associati.
- Paradigma di utilizzo: Non sei assolutamente obbligato a usare classi o ereditarietà per scrivere codice.
- Puoi scrivere script puramente procedurali (una sequenza di istruzioni e funzioni Top-Level senza mai definire una
class). - Puoi usare uno stile funzionale sfruttando funzioni di prima classe, lambda,
map(),filter()e le list comprehension.
- Puoi scrivere script puramente procedurali (una sequenza di istruzioni e funzioni Top-Level senza mai definire una
Esempio Python non-OOP: Puoi scrivere un intero programma di data science definendo semplicemente due funzioni e chiamandole in sequenza, senza creare alcuna classe custom.
2. Mojo: Progettato per essere flessibile e di basso livello
Mojo segue una filosofia simile a Python, ma con la flessibilità tipica di linguaggi come C++ o Rust:
- Funzioni di primo livello (Top-Level Functions): Puoi definire funzioni
fnodefdirettamente a livello di modulo, senza doverle racchiudere all’interno di classi o strutture (a differenza di linguaggi puramente ad oggetti o con boilerplate rigido come Java o C#). structvsclass: Mojo introduce lestruct, che supportano metodi e incapsulamento (tipici della OOP), ma sono concepite per l’allocazione diretta in memoria senza overhead dinamico. Non usano il dispatch dinamico né l’ereditarietà classica dei linguaggi OOP tradizionali.- Orientamento al calcolo vettoriale e procedurale: Nell’ambito AI, gran parte del codice Mojo viene scritta in stile procedurale o funzionale/vettoriale (operazioni su matrici, cicli ottimizzati per hardware, chiamate SIMD), dove l’astrazione ad oggetti classica aggiungerebbe solo complessità inutile.
Confronto sintetico sui paradigmi supportati
| Paradigma | Python | Mojo |
|---|---|---|
| Procedurale (Funzioni e sequenze) | Native e ampiamente usato. | Native, ideale per algoritmi ad alte prestazioni. |
| Orientato agli Oggetti (OOP) | Supportato tramite class (dinamico). | Supportato tramite struct e tipi nativi (statico/compilato). |
| Funzionale | Supportato (lambda, immutabilità parziale). | Supportato (trattamento ad alte prestazioni delle funzioni). |
| Vettoriale / SIMD | Tramite librerie esterne (NumPy/C). | Integrato nativamente nel linguaggio. |
Sia in Python che in Mojo le classi e gli oggetti sono uno strumento a disposizione dello sviluppatore, non un vincolo strutturale.
Vuoi approfondire la differenza tra struct in Mojo e class in Python?
Sì. Per capirsi: si possono usare massicciamente variabili globali vecchio stile?
Sì, tecnicamente si può, ma con significative differenze di comportamento, scopo e impatto sulle prestazioni tra i due linguaggi.
1. In Python: Si può fare, ma è fonte di insidie
In Python puoi definire variabili a livello di modulo (fuori da qualsiasi funzione) e accedervi da qualunque punto. Tuttavia, il linguaggio impone delle regole precise:
- Lettura vs Scrittura (
global): Puoi leggere una variabile globale all’interno di una funzione senza problemi. Se però vuoi modificarla (riassegnarla), devi dichiararla esplicitamente con la parola chiaveglobal, altrimenti Python creerà una nuova variabile locale con lo stesso nome.
Python
# Variabile globale
CONTATORE = 0
def incrementa():
global CONTATORE # Necessario per modificare la variabile globale
CONTATORE += 1
def leggi():
print(CONTATORE) # Lettura diretta, 'global' non serve
Perché evitarne un uso massiccio in Python?
- Thread Safety & Concorrenza: Python usa il GIL (Global Interpreter Lock). Gestire lo stato globale con thread multipli o con l’asincronia (
asyncio) porta facilmente a race conditions difficili da debuggare. - Prestazioni: L’accesso alle variabili nel namespace globale in Python è leggermente più lento rispetto all’accesso alle variabili locali, poiché richiede una ricerca nel dizionario del modulo (
globals()).
2. In Mojo: Si può fare, ma il compilatore impone la sicurezza
Mojo consente di definire variabili a livello di modulo (globali), ma l’approccio è molto più rigido e orientato al controllo della memoria e delle prestazioni:
- Inizializzazione e Mutabilità: Le variabili globali devono essere dichiarate esplicitamente con
var(mutabile) oalias/let(costante a tempo di compilazione). - Gestione della memoria e Tipi: A differenza di Python, una variabile globale mutabile in Mojo vive in una regione di memoria statica. Se usata all’interno di funzioni veloci (
fn), il compilatore verificherà rigorosamente il tipo e la concorrenza.
Snippet di codice
# Costante globale risolta a tempo di compilazione (zero overhead)
alias MAX_CONFIG = 1024
# Variabile globale mutabile
var stato_sistema: Int = 0
fn aggiorna_stato(nuovo_valore: Int):
# Modifica diretta della variabile globale
stato_sistema = nuovo_valore
Perché un uso massiccio è problematico in Mojo? Mojo è progettato per il calcolo parallelo (CPU multithread, GPU, registri SIMD). L’uso massiccio di stato globale mutabile:
- Rompere la parallelizzazione: Se due thread o due blocchi GPU cercano di scrivere sulla stessa variabile globale, si creano conflitti di memoria (data races). I compilatori basati su LLVM/MLIR come quello di Mojo non riescono a ottimizzare o vettorializzare il codice se ci sono dipendenze globali non gestite.
- Ownership e Borrowing: Il sistema di gestione della memoria di Mojo preferisce che i dati vengano passati alle funzioni tramite riferimenti espliciti (
borrowed,inout,owned) anziché letti e scritti di nascosto da uno spazio globale.
Sintesi
Se stai scrivendo uno script procedurale rapido o un prototipo, l’uso di variabili globali “vecchio stile” funziona in entrambi gli ambienti.
Se invece lo scopo è costruire architetture AI o pipeline ad alte prestazioni, l’uso massiccio di variabili globali è sconsigliato: in Python rende il codice fragile e difficile da testare, mentre in Mojo impedisce al compilatore di applicare le ottimizzazioni hardware che rendono il linguaggio così veloce.
Vuoi vedere come Mojo gestisce il passaggio di parametri per riferimento (borrowed e inout)?Sì
Si, grazie.
In Mojo, la gestione dei parametri nelle funzioni fn è pensata per garantire prestazioni da C++ e sicurezza della memoria da Rust, eliminando il bisogno di ricorrere a variabili globali.
Quando definisci una funzione con fn, il compilatore Mojo ti obbliga a chiarire esattamente chi possiede un dato (ownership) e chi può modificarlo. Per farlo, mette a disposizione tre qualificatori principali per i parametri: borrowed, inout e owned.
1. borrowed (Predefinito nelle funzioni fn)
È la modalità standard per le funzioni fn. Passa il parametro come riferimento in sola lettura (read-only reference).
- Come funziona: Il chiamante mantiene la proprietà del dato; la funzione può solo leggerlo senza copiarlo e senza poterlo modificare.
- Vantaggio: Zero overhead di copia (fondamentale quando si passano matrici o tensor enormi in memoria).
Snippet di codice
fn calcola_area(borrowed larghezza: Float64, borrowed altezza: Float64) -> Float64:
# larghezza = 10.0 <-- ERRORE di compilazione! Non si può modificare un parametro 'borrowed'
return larghezza * altezza
fn main():
let w: Float64 = 5.0
let h: Float64 = 4.0
# 'borrowed' è implicito nelle funzioni 'fn', quindi si può anche omettere la parola chiave
let area = calcola_area(w, h)
2. inout (Passaggio per riferimento mutabile)
Corrisponde al passaggio per riferimento mutabile (simile a T& in C++ o &mut T in Rust).
- Come funziona: La funzione riceve un riferimento diretto alla variabile originale del chiamante e può modificarla. Le modifiche fatte dentro la funzione si riflettono direttamente sulla variabile esterna.
- Vantaggio: Evita di riallocare memoria ed evita lo stato globale mutabile: passi la struttura dati alla funzione, essa la aggiorna sul posto (in-place) e la restituisce modificata.
Snippet di codice
fn azzera_contatore(inout valore: Int):
valore = 0 # Modifica direttamente la variabile passata dall'esterno
fn main():
var x: Int = 42
azzera_contatore(x)
print(x) # Stampa: 0
3. owned (Trasferimento di proprietà / Move semantics)
Indica che la funzione vuole prendere la piena proprietà del valore.
- Come funziona: Il chiamante cede il dato alla funzione. Se usi l’operatore di transfer
^(move), la variabile originale nel chiamante viene invalidata e la memoria verrà liberata direttamente dalla funzione alla fine della sua esecuzione. - Vantaggio: Utile quando una funzione deve trasformare un dato consumandolo (ad esempio la trasformazione di un modello di dati o l’elaborazione finale di un buffer).
Snippet di codice
fn elabora_e_distruggi(owned testo: String):
testo += " [Elaborato]"
print(testo)
# Alla fine di questa funzione, 'testo' viene distrutto e la sua memoria liberata
fn main():
var s: String = "Dati di input"
# L'operatore '^' trasferisce la proprietà ed invalida 's' nel chiamante
elabora_e_distruggi(s^)
# print(s) <-- ERRORE di compilazione! 's' non è più valida perché la sua proprietà è stata trasferita
Confronto rapido
| Qualificatore | Permesso di Modifica | Copia i dati? | Chi distrugge il dato? | Analogia C++ / Rust |
|---|---|---|---|---|
borrowed | Solo lettura | No (Riferimento) | Il chiamante | const T& (C++) / &T (Rust) |
inout | Lettura e Scrittura | No (Riferimento) | Il chiamante | T& (C++) / &mut T (Rust) |
owned | Lettura e Scrittura | No (se usato ^) | La funzione chiamata | T&& (C++ std::move) / T (Rust) |
Questo sistema permette a Mojo di garantire velocità nativa e sicurezza nei contesti multithread/GPU, evitando del tutto sia la lentezza della Garbage Collection sia i problemi di concorrenza legati alle vecchie variabili globali.
Vuoi vedere come questo sistema previene i bug nei cicli di calcolo su GPU o vettoriali?