Resa Python, generatori ed espressioni del generatore

In questo tutorial imparerai come creare facilmente iterazioni usando i generatori Python, come è diverso dagli iteratori e dalle normali funzioni e perché dovresti usarlo.

Video: generatori Python

Generatori in Python

C'è molto lavoro nella creazione di un iteratore in Python. Dobbiamo implementare una classe con __iter__()e __next__()metodo, tenere traccia degli stati interni, e sollevare StopIterationquando non ci sono valori da restituire.

Questo è sia lungo che controintuitivo. Il generatore viene in soccorso in tali situazioni.

I generatori Python sono un modo semplice per creare iteratori. Tutto il lavoro che abbiamo menzionato sopra viene gestito automaticamente dai generatori in Python.

In parole semplici, un generatore è una funzione che restituisce un oggetto (iteratore) su cui possiamo iterare (un valore alla volta).

Crea generatori in Python

È abbastanza semplice creare un generatore in Python. È facile come definire una funzione normale, ma con yieldun'istruzione invece di returnun'istruzione.

Se una funzione contiene almeno yieldun'istruzione (può contenere altre istruzioni yieldo return), diventa una funzione generatore. Entrambi yielde returnrestituiranno un valore da una funzione.

La differenza è che mentre returnun'istruzione termina completamente una funzione, l' yieldistruzione mette in pausa la funzione salvando tutti i suoi stati e successivamente continua da lì alle chiamate successive.

Differenze tra la funzione generatore e la funzione normale

Ecco come una funzione del generatore differisce da una funzione normale.

  • La funzione generatore contiene una o più yieldistruzioni.
  • Quando viene chiamato, restituisce un oggetto (iteratore) ma non avvia l'esecuzione immediatamente.
  • Metodi come __iter__()e __next__()vengono implementati automaticamente. Quindi possiamo iterare attraverso gli elementi usando next().
  • Una volta che la funzione cede, la funzione viene messa in pausa e il controllo viene trasferito al chiamante.
  • Le variabili locali e il loro stato vengono ricordati tra le chiamate successive.
  • Infine, quando la funzione termina, StopIterationviene sollevata automaticamente alle successive chiamate.

Ecco un esempio per illustrare tutti i punti sopra indicati. Abbiamo una funzione generatore denominata my_gen()con diverse yieldistruzioni.

 # A simple generator function def my_gen(): n = 1 print('This is printed first') # Generator function contains yield statements yield n n += 1 print('This is printed second') yield n n += 1 print('This is printed at last') yield n

Di seguito viene fornita un'esecuzione interattiva nell'interprete. Eseguili nella shell Python per vedere l'output.

 >>> # It returns an object but does not start execution immediately. >>> a = my_gen() >>> # We can iterate through the items using next(). >>> next(a) This is printed first 1 >>> # Once the function yields, the function is paused and the control is transferred to the caller. >>> # Local variables and theirs states are remembered between successive calls. >>> next(a) This is printed second 2 >>> next(a) This is printed at last 3 >>> # Finally, when the function terminates, StopIteration is raised automatically on further calls. >>> next(a) Traceback (most recent call last):… StopIteration >>> next(a) Traceback (most recent call last):… StopIteration

Una cosa interessante da notare nell'esempio precedente è che il valore della variabile n viene ricordato tra ogni chiamata.

A differenza delle normali funzioni, le variabili locali non vengono distrutte quando la funzione cede. Inoltre, l'oggetto generatore può essere iterato una sola volta.

Per riavviare il processo dobbiamo creare un altro oggetto generatore usando qualcosa di simile a = my_gen().

Un'ultima cosa da notare è che possiamo usare direttamente i generatori con i cicli for.

Questo perché un forciclo prende un iteratore e lo ripete usando la next()funzione. Termina automaticamente quando StopIterationviene sollevato. Controlla qui per sapere come un ciclo for è effettivamente implementato in Python.

 # A simple generator function def my_gen(): n = 1 print('This is printed first') # Generator function contains yield statements yield n n += 1 print('This is printed second') yield n n += 1 print('This is printed at last') yield n # Using for loop for item in my_gen(): print(item)

Quando esegui il programma, l'output sarà:

 Viene stampato per primo 1 Viene stampato per secondo 2 Viene stampato per ultimo 3

Generatori Python con un ciclo

L'esempio sopra è di minore utilità e lo abbiamo studiato solo per avere un'idea di ciò che stava accadendo in background.

Normalmente, le funzioni del generatore sono implementate con un loop avente una condizione di terminazione adeguata.

Facciamo un esempio di un generatore che inverte una stringa.

 def rev_str(my_str): length = len(my_str) for i in range(length - 1, -1, -1): yield my_str(i) # For loop to reverse the string for char in rev_str("hello"): print(char)

Produzione

 olleh

In questo esempio, abbiamo utilizzato la range()funzione per ottenere l'indice in ordine inverso utilizzando il ciclo for.

Nota : questa funzione di generazione non funziona solo con le stringhe, ma anche con altri tipi di iterabili come lista, tupla, ecc.

Python Generator Expression

Generatori semplici possono essere facilmente creati al volo utilizzando le espressioni del generatore. Rende facile la costruzione di generatori.

Simile alle funzioni lambda che creano funzioni anonime, le espressioni del generatore creano funzioni del generatore anonimo.

La sintassi per l'espressione del generatore è simile a quella di una comprensione di elenchi in Python. Ma le parentesi quadre vengono sostituite con parentesi tonde.

La principale differenza tra una comprensione dell'elenco e un'espressione generatore è che una comprensione dell'elenco produce l'intero elenco mentre l'espressione generatore produce un elemento alla volta.

They have lazy execution ( producing items only when asked for ). For this reason, a generator expression is much more memory efficient than an equivalent list comprehension.

 # Initialize the list my_list = (1, 3, 6, 10) # square each term using list comprehension list_ = (x**2 for x in my_list) # same thing can be done using a generator expression # generator expressions are surrounded by parenthesis () generator = (x**2 for x in my_list) print(list_) print(generator)

Output

 (1, 9, 36, 100) 

We can see above that the generator expression did not produce the required result immediately. Instead, it returned a generator object, which produces items only on demand.

Here is how we can start getting items from the generator:

 # Initialize the list my_list = (1, 3, 6, 10) a = (x**2 for x in my_list) print(next(a)) print(next(a)) print(next(a)) print(next(a)) next(a)

When we run the above program, we get the following output:

 1 9 36 100 Traceback (most recent call last): File "", line 15, in StopIteration

Generator expressions can be used as function arguments. When used in such a way, the round parentheses can be dropped.

 >>> sum(x**2 for x in my_list) 146 >>> max(x**2 for x in my_list) 100

Use of Python Generators

There are several reasons that make generators a powerful implementation.

1. Easy to Implement

Generators can be implemented in a clear and concise way as compared to their iterator class counterpart. Following is an example to implement a sequence of power of 2 using an iterator class.

 class PowTwo: def __init__(self, max=0): self.n = 0 self.max = max def __iter__(self): return self def __next__(self): if self.n> self.max: raise StopIteration result = 2 ** self.n self.n += 1 return result

The above program was lengthy and confusing. Now, let's do the same using a generator function.

 def PowTwoGen(max=0): n = 0 while n < max: yield 2 ** n n += 1

Since generators keep track of details automatically, the implementation was concise and much cleaner.

2. Memory Efficient

A normal function to return a sequence will create the entire sequence in memory before returning the result. This is an overkill, if the number of items in the sequence is very large.

Generator implementation of such sequences is memory friendly and is preferred since it only produces one item at a time.

3. Represent Infinite Stream

I generatori sono mezzi eccellenti per rappresentare un flusso infinito di dati. I flussi infiniti non possono essere archiviati in memoria e poiché i generatori producono solo un elemento alla volta, possono rappresentare un flusso infinito di dati.

La seguente funzione generatore può generare tutti i numeri pari (almeno in teoria).

 def all_even(): n = 0 while True: yield n n += 2

4. Generatori di pipelining

È possibile utilizzare più generatori per convogliare una serie di operazioni. Questo è meglio illustrato utilizzando un esempio.

Supponiamo di avere un generatore che produce i numeri nella serie di Fibonacci. E abbiamo un altro generatore per la quadratura dei numeri.

Se vogliamo scoprire la somma dei quadrati dei numeri nella serie di Fibonacci, possiamo farlo nel modo seguente pipeline insieme l'output delle funzioni del generatore.

 def fibonacci_numbers(nums): x, y = 0, 1 for _ in range(nums): x, y = y, x+y yield x def square(nums): for num in nums: yield num**2 print(sum(square(fibonacci_numbers(10))))

Produzione

 4895

Questo pipelining è efficiente e facile da leggere (e sì, molto più interessante!).

Articoli interessanti...