Generatori in Python

Generatori in Python

Questo articolo spiega i generatori in Python.

YouTube Video

Generatori in Python

Panoramica

I generatori in Python sono un tipo di iteratore e una funzionalità potente per eseguire elaborazioni ripetitive in modo efficiente. Ti permettono di scrivere codice efficiente in termini di memoria quando si lavora con grandi quantità di dati.

Cos’è un generatore?

Un generatore in Python è una funzione speciale che produce un valore alla volta, definita utilizzando la parola chiave yield. La sua caratteristica è che mette in pausa l'esecuzione mantenendo lo stato e può riprendere in seguito.

Basi di yield

yield è una parola chiave che restituisce un valore e mette in pausa l'esecuzione della funzione allo stesso tempo.

 1def simple_generator():
 2    yield 1
 3    yield 2
 4    yield 3
 5
 6gen = simple_generator()
 7
 8print(next(gen))  # 1
 9print(next(gen))  # 2
10print(next(gen))  # 3
  • Quando viene chiamata, questa funzione restituisce un oggetto generatore che produce i valori uno alla volta.
  • Se chiami next() quando non c'è un valore successivo, si verificherà un errore StopIteration.

next() e StopIteration

 1def simple_generator():
 2    yield 1
 3    yield 2
 4    yield 3
 5
 6gen = simple_generator()
 7
 8try:
 9    while True:
10        value = next(gen)
11        print(value)
12except StopIteration:
13    print("Finished")
  • Gestendo esplicitamente l'errore StopIteration in questo modo, puoi rilevare quando un generatore è terminato.

send(value)

Chiamare send(value) riprende il generatore e invia value alla posizione dell'espressione yield. Il valore inviato può essere ricevuto nel generatore come valore di ritorno dell'espressione yield. Alla prima chiamata, non puoi inviare nulla tranne None con send(value), quindi devi usare next() o send(None).

 1def gen():
 2    x = yield 1
 3    print(f"x = {x}")
 4    y = yield 2
 5    print(f"y = {y}")
 6
 7g = gen()
 8print(next(g))       # -> 1 (value from yield 1)
 9print(g.send(10))    # -> x = 10, 2 (value from yield 2)
10print(g.send(20))    # -> y = 20, StopIteration occurs
  • Con send(10), il yield del generatore diventa un'espressione che restituisce 10, e 10 viene assegnato a x.

throw()

Chiamare throw riprende il generatore e solleva un'eccezione nella posizione del yield in pausa. Puoi gestire l'eccezione all'interno del generatore per continuare l'elaborazione. Se l'eccezione non viene intercettata, si propaga all'esterno e il generatore termina.

 1def gen():
 2    try:
 3        yield 1
 4    except ValueError as e:
 5        print(f"Caught: {e}")
 6        yield "recovered"
 7
 8g = gen()
 9print(next(g))   # -> 1
10print(g.throw(ValueError("boom")))  # -> Caught: boom, "recovered"
  • In questo codice, throw viene chiamato per iniettare un'eccezione nel generatore. Dal lato del generatore, l'eccezione viene gestita e viene restituito recovered.

close()

Chiamare close() termina il generatore. All'interno del generatore puoi eseguire attività di pulizia usando finally. Chiamare next() o send() dopo aver chiamato close() genera un errore di tipo StopIteration.

1def gen():
2    try:
3        yield 1
4    finally:
5        print("Cleaning up...")
6
7g = gen()
8print(next(g))  # -> 1
9g.close()       # -> Cleaning up...
  • Questo codice mostra che chiamare close() termina il generatore e attiva il processo di pulizia in finally.

yield from

yield from è una sintassi usata per delegare a un subgeneratore. È un modo semplice per chiamare un altro generatore all'interno di un generatore e passare tutti i suoi valori all'ambito esterno.

1def sub_gen():
2    yield 1
3    yield 2
4
5def main_gen():
6    yield from sub_gen()
7    yield 3
8
9print(list(main_gen()))  # -> [1, 2, 3]
  • Questo codice delega tutti i valori dal subgeneratore al generatore esterno usando yield from e poi produce 3.

Relazione con gli iteratori

I generatori implementano internamente __iter__() e __next__(), rendendoli un tipo di iteratore. Perciò sono completamente compatibili con operazioni iterabili come i cicli for.

Integrazione con i cicli for

In Python, un ciclo for utilizza internamente next() per ottenere automaticamente i valori.

1def simple_generator():
2    yield 1
3    yield 2
4    yield 3
5
6for value in simple_generator():
7    print(value)

Con questo metodo, la gestione di StopIteration è anch'essa automatica.

Creazione di generatori infiniti

1def count_up(start=0):
2    while True:
3        yield start
4        start += 1
5
6counter = count_up()
7print(next(counter))  # 0
8print(next(counter))  # 1

È possibile creare cicli infiniti, ma occorre prestare attenzione nell'utilizzarli.

Espressioni generatrici

Le espressioni generatrici, scritte usando le parentesi tonde, consentono di definire generatori con una sintassi simile alle list comprehension.

1# List comprehension (generates the entire list at once)
2squares_list = [x**2 for x in range(5)]
3print(squares_list)
4
5# Generator expression
6squares_gen = (x**2 for x in range(5))
7for square in squares_gen:
8    print(square)

A differenza delle list comprehension, non caricano tutti gli elementi in memoria simultaneamente, risultando più efficienti nella gestione della memoria.

Gestione degli errori nei generatori

Possono verificarsi eccezioni all'interno di un generatore. In questi casi, si utilizza try-except proprio come nel normale codice Python.

 1def safe_divide_generator(numbers, divisor):
 2    """Yields results of dividing numbers by a given divisor safely."""
 3    for number in numbers:
 4        try:
 5            yield number / divisor  # Attempt to divide and yield result.
 6        except ZeroDivisionError:
 7            yield float('inf')  # Return infinity if division by zero occurs.
 8
 9# Example usage
10numbers = [10, 20, 30]
11gen = safe_divide_generator(numbers, 0)  # Create generator with divisor as 0.
12for value in gen:
13    print(value)  # Output: inf, inf, inf

In questo esempio, viene effettuata la gestione corretta degli errori in caso di divisione per zero.

Traccia dello stack di un generatore

Se si verifica un'eccezione all'interno del generatore, verrà sollevata quando il generatore viene ripreso.

 1def error_generator():
 2    """A generator that yields values and raises an error."""
 3    yield 1
 4    raise ValueError("An error occurred")  # Raise a ValueError intentionally.
 5    yield 2
 6
 7gen = error_generator()
 8print(next(gen))       # Output: 1 (first value yielded)
 9try:
10    print(next(gen))   # Attempt to get the next value, which raises an error
11except ValueError as e:
12    print(e)           # Output: An error occurred (exception message is printed)
  • Questo generatore restituisce prima 1. L'errore sollevato durante la ripresa viene intercettato e mostrato come messaggio di errore.

Esempi di utilizzo dei generatori

Lettura di un file riga per riga (adatto a file di grandi dimensioni)

1def read_large_file(filepath):
2    with open(filepath, 'r') as f:
3        for line in f:
4            yield line.strip()
  • Questa funzione legge un file di testo riga per riga usando un iteratore, rimuove gli spazi bianchi da ogni riga e la restituisce come generatore, permettendo di elaborare file di grandi dimensioni con un uso ridotto di memoria.

Generatore per la sequenza di Fibonacci

1def fibonacci(limit):
2    a, b = 0, 1
3    while a < limit:
4        yield a
5        a, b = b, a + b
6
7for n in fibonacci(100):
8    print(n)
  • Questo codice utilizza un generatore per generare in sequenza i numeri di Fibonacci inferiori al limite massimo e li visualizza usando un ciclo for.

Casi d'uso

I generatori possono anche essere utilizzati nei seguenti scenari.

  • Elaborazione sequenziale di grandi file CSV o di log
  • Paginazione API
  • Elaborazione di dati in streaming (ad esempio, Kafka, dispositivi IoT)

Riepilogo

Concetto Punto chiave
yield Mette in pausa e restituisce un valore
Funzione generatrice Una funzione che contiene yield e restituisce un iteratore quando viene chiamata
Vantaggi Efficiente in termini di memoria e ideale per l'elaborazione di grandi insiemi di dati
Espressione generatrice Permette una sintassi concisa come (x for x in iterable)

Utilizzando i generatori, puoi elaborare grandi set di dati in modo efficiente risparmiando memoria e mantenendo il codice conciso.

Puoi seguire l'articolo sopra utilizzando Visual Studio Code sul nostro canale YouTube. Controlla anche il nostro canale YouTube.

YouTube Video