In un precedente articolo (qui) ho proposto un semplice test, il FizzBuzz, per valutare le capacità di un candidato programmatore. In questo articolo, invece, vediamo come il candidato potrebbe stupire l'esaminatore, risolvendo il problema con l'uso di una funzione generatore (generator function) di Python.

Partiamo dal testo del problema:

Scrivi un programma che stampa a video i numeri da 1 a 100. Per i numeri multipli di 3 stampa "Fizz" mentre per i multipli di 5 stampa "Buzz". Per i numeri che sono multipli sia di 3 che di 5 stampa "FizzBuzz".

La soluzione che si potrebbe dare in Python è la seguente (per una descrizione della soluzione si faccia riferimento a questo articolo):

def fizzbuzz(n):
    next3 = 3
    next5 = 5

    for i in range(1,n+1):
        if((i==next3) and (i==next5)):
            print(str(i)+" FizzBuzz")
            next3 += 3
            next5 += 5
        elif (i == next3):
            print (str(i)+" Fizz")
            next3 += 3
        elif(i==next5):
            print(str(i)+" Buzz")
            next5 += 5
        else:
            print(str(i))

fizzbuzz(100)

Se eseguito, lo script produce il seguente output:

1
2
3 Fizz
4
5 Buzz
6 Fizz
7
8
9 Fizz
10 Buzz
11
12 Fizz
13
14
15 FizzBuzz
16
...
98 99 Fizz 100 Buzz

La soluzione presentata qui sopra è già un'ottima risposta: il candidato dimostra di riuscire a ragionare lucidamente, anche sotto pressione!!!

Se però vogliamo stupire l'esaminatore possiamo cercare di fare qualcosa in più, magari separare la pura e semplice generazione della sequenza dalla sua rappresentazione a video. Un sistema potrebbe essere quello di restituire una lista mentre un altro sistema potrebbe essere quello di scrivere una funzione generatore.

COSA SONO LE FUNZIONI GENERATORE (GENERATOR FUNCTIONS)?

Per restituire una sequenza in Python si possono utilizzare le funzioni generatore, ovvero funzioni che ritornano un iteratore; banalmente, un iteratore è un oggetto su cui è possibile iterare, tipicamente con un ciclo for.
Quindi una funzione generatore può essere vista come una funzione che viene chiamata "step by step" restituendo ogni volta il valore successivo di una sequenza.

QUALI SONO I VANTAGGI?

Il vantaggio principale è il risparmio di memoria: lavorando un elemento alla volta permette di non dover immagazzinare l'intera sequenza. Nel nostro caso l'occupazione di memoria sarebbe minima, quindi è un vantaggio ininfluente, ma in altri casi il risparmio di memoria potrebbe essere determinante.

Il fatto di non dover immagazzinare l'intera sequenza ci porta ad un secondo vantaggio: nel caso di una sequenza corposa di valori (per non parlare di un sequenza potenzialmente infinita) ci permette di iniziare subito ad analizzare gli elementi, prima di aver terminato la generazione. Questo, in alcuni casi, potrebbe semplicare la gestione di un'interfaccia utente o permettere la gestione di flussi continui di dati.

COME POSSIAMO RISCRIVERE IL FIZZBUZZ?

Veniamo ora al dunque.
Qui sotto si trova la versione modificata: in pratica, rispetto alla versione precedente, abbiamo sostituito le print() con l'istruzione yield().
L'istruzione yield() è paragonabile ad un return, con la differenza che la chiamata successiva riparte dall'istruzione che segue yield().
Sostanzialmente, Python gestisce per noi una "macchina a stati", ricordandosi di volta in volta il punto in cui era uscito con yield() e ripartendo da quello nella chiamata successiva.

def fizzbuzz_gen(n):
    next3 = 3
    next5 = 5

    for i in range(1,n+1):
        if((i==next3) and (i==next5)):
            yield((i,"FizzBuzz"))
            next3 += 3
            next5 += 5
        elif (i == next3):
            yield ((i, "Fizz"))
            next3 += 3
        elif(i==next5):
            yield((i,"Buzz"))
            next5 += 5
        else:
            yield((i, ""))

fizzbuzz_gen(100)                 #1
print (fizzbuzz_gen(100))         #2
print (list(fizzbuzz_gen(100)))   #3
for cnt in fizzbuzz_gen(100):     #4
    print(cnt)

Se eseguito, lo script produce il seguente output:

<generator object fizzbuzz_gen at 0x00000294A4506CC8>
[(1, ''), (2, ''), (3, 'Fizz'), ..., (100, 'Buzz')]
(1, '')
(2, '')
(3, 'Fizz')
...
(100, 'Buzz')

La semplice chiamata a fizzbuzz_gen() (#1) non produce alcun output perchè, a differenza della fizzbuzz() presentata inizialmente, questa funzione non si occupa di stampare a video la sequenza; infatti abbiamo separato la generazione della sequenza dalla sua rappresentazione a video.
Nella seconda chiamata (#2) possiamo notare come la funzione fizzbuzz_gen() restituisce un oggetto generatore.
L'output della funzione è utilizzabile creando una lista (vedi esempio #3) oppure utilizzando un ciclo for (vedi esempio #4).

CONCLUSIONI

Per concludere, le funzioni generatore sono un'arma in più per scrivere codice lineare e al tempo stesso ottimizzato nell'esecuzione.
La semplicità di utilizzo ne fa uno strumento da conoscere assolutamente!

FONTI

Suggerisco una visita a questa pagina https://pythonpertutti.it/blog/python-generatori/ e ai relativi link; io ci ho trovato delle ottime spiegazioni, puntuali ed esaustive.