🚀 Nuova versione beta disponibile! Feedback o problemi? Contattaci

Generatori in Python

Codegrind Team•Sep 11 2024

I generatori in Python sono un modo speciale per creare iteratori in modo semplice ed efficiente. A differenza delle funzioni tradizionali, che restituiscono un valore e terminano, i generatori possono “ricordare” lo stato in cui si trovano e riprendere l’esecuzione successiva da quel punto, rendendoli particolarmente adatti a lavorare con grandi quantità di dati o sequenze infinite. In questo articolo esploreremo cosa sono i generatori, come funzionano e come possono migliorare l’efficienza del tuo codice.

Cos’è un Generatore?

Un generatore è una funzione che restituisce un iteratore. A differenza di una funzione normale che utilizza la parola chiave return, un generatore utilizza yield per restituire valori uno alla volta. Quando viene chiamato il metodo next() su un generatore, l’esecuzione della funzione riprende dal punto in cui si era interrotta, continuando fino alla successiva espressione yield.

Differenza tra Funzioni e Generatori

  • Funzioni: Restituiscono un valore e terminano.
  • Generatori: Restituiscono valori uno alla volta con yield e “ricordano” lo stato tra una chiamata e l’altra.

Ecco un semplice esempio di generatore che genera numeri da 1 a 3:

def semplice_generatore():
    yield 1
    yield 2
    yield 3

gen = semplice_generatore()
print(next(gen))  # Output: 1
print(next(gen))  # Output: 2
print(next(gen))  # Output: 3

In questo esempio, la funzione semplice_generatore utilizza yield per restituire valori uno alla volta. Quando viene chiamato next() su gen, il generatore riprende l’esecuzione da dove si era interrotto.

Quando Utilizzare i Generatori?

I generatori sono particolarmente utili quando:

  1. Hai a che fare con grandi quantitĂ  di dati o sequenze infinite, dove caricare tutti i dati in memoria contemporaneamente sarebbe inefficiente.
  2. Vuoi ritardare l’esecuzione o la creazione di dati fino a quando non sono realmente necessari (lazy evaluation).

Esempio con una Sequenza Infinita

Ecco un esempio di un generatore che crea una sequenza infinita di numeri interi:

def numeri_infinito():
    n = 0
    while True:
        yield n
        n += 1

gen = numeri_infinito()
for _ in range(5):
    print(next(gen))  # Output: 0, 1, 2, 3, 4

Questo generatore non termina mai, poiché continua a generare numeri in modo infinito. Ogni volta che viene chiamato next(), restituisce il valore successivo.

Funzionamento di yield

La parola chiave yield è il cuore dei generatori. Quando una funzione incontra yield, restituisce un valore e “pausa” l’esecuzione, mantenendo il contesto (stato delle variabili locali). Alla chiamata successiva, il generatore riprende l’esecuzione da dove era stato interrotto.

Esempio: Generatore di Fibonacci

Ecco un esempio di un generatore che calcola i numeri della sequenza di Fibonacci:

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

gen = fibonacci()
for _ in range(10):
    print(next(gen))  # Output: primi 10 numeri di Fibonacci

In questo caso, ogni chiamata a next() restituisce il prossimo numero di Fibonacci. Il generatore continua a generare numeri Fibonacci senza dover memorizzare l’intera sequenza.

Generatori e Cicli for

I generatori sono particolarmente utili con i cicli for. Poiché i cicli for richiedono un iteratore per funzionare, possono lavorare direttamente con i generatori senza la necessità di chiamare next() manualmente.

Esempio con Ciclo for

def conta_fino_a(n):
    numero = 1
    while numero <= n:
        yield numero
        numero += 1

for numero in conta_fino_a(5):
    print(numero)

Output:

1
2
3
4
5

In questo esempio, il ciclo for itera automaticamente attraverso i valori generati da conta_fino_a, chiamando next() dietro le quinte fino a quando il generatore non si esaurisce.

Generatori con return

Normalmente, i generatori terminano quando non ci sono più valori da generare. Tuttavia, puoi utilizzare return per interrompere il generatore prima del previsto. Quando viene eseguito un return in un generatore, viene sollevata un’eccezione StopIteration per indicare che il generatore è terminato.

Esempio con return

def fino_a_n(n):
    for i in range(n):
        yield i
    return "Fine del generatore"

gen = fino_a_n(3)
for numero in gen:
    print(numero)

Output:

0
1
2

In questo esempio, il return termina il generatore una volta raggiunto n, ma il valore restituito dal return non viene passato all’esterno del generatore.

Comprensioni di Generatori

Le comprensioni di generatori (generator comprehensions) sono simili alle liste comprese, ma invece di creare una lista, creano un generatore. Questo è utile quando vuoi iterare su una sequenza senza caricarla interamente in memoria.

Esempio di Comprensione di Generatori

gen = (x**2 for x in range(5))

for numero in gen:
    print(numero)

Output:

0
1
4
9
16

A differenza delle liste comprese, le comprensioni di generatori non creano immediatamente una lista, ma generano valori uno alla volta quando necessario.

Vantaggi dei Generatori

  1. Efficienza della memoria: I generatori non caricano in memoria tutti i dati contemporaneamente, il che è utile per lavorare con grandi dataset o sequenze infinite.
  2. Lazy Evaluation: I generatori generano i dati solo quando vengono richiesti, ritardando l’esecuzione fino al momento in cui i dati sono necessari.
  3. Performance: Grazie al loro utilizzo di yield, i generatori sono spesso piĂą veloci rispetto a soluzioni che creano e memorizzano liste complete.

Gestione delle Eccezioni nei Generatori

Puoi gestire eccezioni all’interno dei generatori proprio come nelle normali funzioni. Ad esempio, puoi usare un blocco try-except per gestire situazioni anomale.

Esempio di Gestione delle Eccezioni

def generatore_sicuro():
    for i in range(5):
        try:
            yield i
        except GeneratorExit:
            print("Generatore terminato")
            raise

gen = generatore_sicuro()
print(next(gen))  # Output: 0
gen.close()  # Termina il generatore, sollevando l'eccezione GeneratorExit

Quando si chiama close() su un generatore, Python solleva un’eccezione GeneratorExit per indicare che il generatore deve essere chiuso.

Generatori e Funzioni yield from

Python offre una sintassi avanzata chiamata yield from per delegare la generazione di valori a un sottogeneratore. Questo è utile quando vuoi suddividere la logica del generatore in più funzioni.

Esempio con yield from

def sotto_generatore():
    yield 1
    yield 2

def generatore_principale():
    yield from sotto_generatore()
    yield 3

for numero in generatore_principale():
    print(numero)

Output:

1
2
3

Con yield from, puoi delegare la generazione di valori da un generatore secondario (sotto_generatore) al generatore principale (generatore_principale).

Conclusione

I generatori sono uno strumento potente in Python per gestire sequenze di dati in modo efficiente. Offrono una soluzione elegante per lavorare con grandi quantità di dati o sequenze infinite, consentendo di generare valori solo quando necessario e migliorando l’efficienza della memoria. Usare i generatori ti permette di scrivere codice più pulito, efficiente e scalabile.