Python ed AI

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

CaratteristicaPythonMojo
EsecuzioneInterpretato (Bytecode / CPython)Compilato direttamente in codice macchina (tramite MLIR/LLVM)
TipizzazioneDinamica (i type hints sono solo decorativi)Statica e forte con fn (opzionalmente dinamica con def)
Gestione MemoriaGarbage Collector / Reference CountingOwnership/Borrowing e allocazione manuale/automatica senza GC
PrestazioniModerate (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.

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 fn o def direttamente 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#).
  • struct vs class: Mojo introduce le struct, 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

ParadigmaPythonMojo
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).
FunzionaleSupportato (lambda, immutabilità parziale).Supportato (trattamento ad alte prestazioni delle funzioni).
Vettoriale / SIMDTramite 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 chiave global, 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?

  1. 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.
  2. 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) o alias / 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:

  1. 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.
  2. 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

QualificatorePermesso di ModificaCopia i dati?Chi distrugge il dato?Analogia C++ / Rust
borrowedSolo letturaNo (Riferimento)Il chiamanteconst T& (C++) / &T (Rust)
inoutLettura e ScritturaNo (Riferimento)Il chiamanteT& (C++) / &mut T (Rust)
ownedLettura e ScritturaNo (se usato ^)La funzione chiamataT&& (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?