Iteratori fantastici e come realizzarli

Foto di John Matychuk su Unsplash

Il problema

Durante l'apprendimento a Make School ho visto i miei colleghi scrivere funzioni che creano elenchi di elementi.

s = 'baacabcaab'
p = 'a'
def find_char (stringa, carattere):
  index = list ()
  per indice, str_char in enumerate (stringa):
    se str_char == carattere:
      indices.append (indice)
  indici di rendimento
print (find_char (s, p)) # [1, 2, 4, 7, 8]

Questa implementazione funziona, ma pone alcuni problemi:

  • E se vogliamo solo il primo risultato; avremo bisogno di svolgere una funzione completamente nuova?
  • Che cosa succede se tutto ciò che facciamo è passare in rassegna il risultato una volta, dobbiamo memorizzare ogni elemento in memoria?

Gli iteratori sono la soluzione ideale a questi problemi. Funzionano come "liste pigre" in quanto invece di restituire una lista con ogni valore che produce e restituisce ogni elemento uno alla volta.

Iteratori restituiscono pigramente valori; risparmio di memoria.

Quindi tuffiamoci nell'apprendimento di loro!

Iteratori integrati

Gli iteratori che sono più spesso sono enumerate () e zip (). Entrambi pigramente restituiscono valori di next () con loro.

range (), tuttavia, non è un iteratore, ma un "pigro iterabile". - Spiegazione

Possiamo convertire range () in un iteratore con iter (), quindi lo faremo per i nostri esempi per motivi di apprendimento.

my_iter = iter (range (10))
print (successivo (my_iter)) # 0
print (next (my_iter)) # 1

Ad ogni chiamata di next () otteniamo il valore successivo nel nostro intervallo; ha senso vero? Se vuoi convertire un iteratore in un elenco, devi solo dargli il costruttore della lista.

my_iter = iter (range (10))
print (list (my_iter)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Se imitiamo questo comportamento, inizieremo a capire di più su come funzionano gli iteratori.

my_iter = iter (range (10))
my_list = list ()
provare:
  mentre vero:
    my_list.append (accanto (my_iter))
eccetto StopIteration:
  passaggio
print (my_list) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Puoi vedere che dovevamo inserirlo in una dichiarazione try catch. Questo perché gli iteratori aumentano StopIteration quando sono stati esauriti.

Quindi, se chiamiamo il prossimo sul nostro iteratore di gamma esaurito, avremo questo errore.

next (my_iter) # Raises: StopIteration

Fare un iteratore

Proviamo a creare un iteratore che si comporta come intervallo con solo l'argomento stop utilizzando tre tipi comuni di iteratori: Classi, Funzioni generatore (Rendimento) ed Espressioni generatore

Classe

Il vecchio modo di creare un iteratore era attraverso una classe esplicitamente definita. Perché un oggetto sia un iteratore, deve implementare __iter __ () che restituisce se stesso e __next __ () che restituisce il valore successivo.

classe my_range:
  _current = -1
  def __init __ (auto, stop):
    self._stop = stop
  def __iter __ (self):
    restituire se stessi
  def __next __ (self):
    self._current + = 1
    se self._current> = self._stop:
      alzare StopIteration
    restituisce self._current
r = my_range (10)
print (list (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Non è stato troppo difficile, ma sfortunatamente, dobbiamo tenere traccia delle variabili tra le chiamate di next (). Personalmente, non mi piace il boilerplate o cambiare il modo in cui penso ai loop perché non è una soluzione drop-in, quindi preferisco i generatori

Il vantaggio principale è che possiamo aggiungere funzioni aggiuntive che modificano le sue variabili interne come _stop o creano nuovi iteratori.

Gli iteratori di classe hanno il rovescio della medaglia di aver bisogno del boilerplate, tuttavia possono avere funzioni aggiuntive che modificano lo stato.

generatori

PEP 255 ha introdotto "generatori semplici" usando la parola chiave yield.

Oggi, i generatori sono iteratori che sono semplicemente più facili da realizzare rispetto alle loro controparti di classe.

Funzione generatore

Le funzioni del generatore sono ciò che alla fine è stato discusso in quel PEP e sono il mio tipo preferito di iteratore, quindi cominciamo con quello.

def my_range (stop):
  indice = 0
  mentre index 
r = my_range (10)
print (list (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Vedi quanto sono belle quelle 4 righe di codice? È leggermente più breve della nostra implementazione di elenchi per finire!

Il generatore funziona con iteratori con meno boilerplate rispetto alle classi con un flusso logico normale.

Le funzioni del generatore "sospendono" automaticamente l'esecuzione e restituiscono il valore specificato ad ogni chiamata di next (). Ciò significa che nessun codice viene eseguito fino alla prima chiamata successiva ().

Ciò significa che il flusso è così:

  1. next () si chiama,
  2. Il codice viene eseguito fino alla dichiarazione di rendimento successiva.
  3. Viene restituito il valore a destra del rendimento.
  4. L'esecuzione è in pausa.
  5. 1–5 ripetizione per ogni chiamata successiva () fino a quando non viene raggiunta l'ultima riga di codice.
  6. StopIteration viene generato.

Le funzioni del generatore consentono inoltre di utilizzare il rendimento della parola chiave che il prossimo () futuro chiamerà a un altro iterabile fino a quando detto iterabile non sarà esaurito.

def yielded_range ():
  rendimento da my_range (10)
print (list (yielded_range ())) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Non è stato un esempio particolarmente complesso. Ma puoi anche farlo in modo ricorsivo!

def my_range_recursive (stop, current = 0):
  se corrente> = stop:
    ritorno
  cedere la corrente
  rendimento da my_range_recursive (stop, corrente + 1)
r = my_range_recursive (10)
print (list (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Espressione del generatore

Le espressioni del generatore ci consentono di creare iteratori come una riga e sono utili quando non è necessario assegnargli funzioni esterne. Sfortunatamente, non possiamo creare un altro my_range usando un'espressione, ma possiamo lavorare su iterable come la nostra ultima funzione my_range.

my_doubled_range_10 = (x * 2 per x in my_range (10))
print (list (my_doubled_range_10)) # 0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

La cosa bella di questo è che fa quanto segue:

  1. L'elenco chiede a my_doubled_range_10 il valore successivo.
  2. my_doubled_range_10 chiede a my_range il valore successivo.
  3. my_doubled_range_10 restituisce il valore di my_range moltiplicato per 2.
  4. L'elenco aggiunge il valore a se stesso.
  5. 1–5 ripetizione fino a quando my_doubled_range_10 genera StopIteration che si verifica quando my_range lo fa.
  6. Viene restituito l'elenco contenente ogni valore restituito da my_doubled_range.

Possiamo persino fare il filtraggio usando le espressioni del generatore!

my_even_range_10 = (x per x in my_range (10) se x% 2 == 0)
print (list (my_even_range_10)) # [0, 2, 4, 6, 8]

Questo è molto simile al precedente, tranne my_even_range_10 restituisce solo valori che corrispondono alla condizione data, quindi solo valori compresi nell'intervallo [0, 10).

In tutto questo, creiamo un elenco solo perché gliel'abbiamo detto.

Il vantaggio

fonte

Poiché i generatori sono iteratori, gli iteratori sono iterabili e gli iteratori restituiscono pigramente valori. Ciò significa che usando questa conoscenza possiamo creare oggetti che ci daranno oggetti solo quando li chiederemo e per quanti ne desideriamo.

Ciò significa che possiamo trasferire i generatori in funzioni che si riducono a vicenda.

print (sum (my_range (10))) # 45

Il calcolo della somma in questo modo evita di creare un elenco quando tutto ciò che stiamo facendo è aggiungerli insieme e quindi scartarli.

Possiamo riscrivere il primo esempio per essere molto meglio usando una funzione di generatore!

s = 'baacabcaab'
p = 'a'
def find_char (stringa, carattere):
  per indice, str_char in enumerate (stringa):
    se str_char == carattere:
      indice di rendimento
print (list (find_char (s, p))) # [1, 2, 4, 7, 8]

Ora immediatamente non ci potrebbero essere evidenti benefici, ma andiamo alla mia prima domanda: "E se volessimo solo il primo risultato; avremo bisogno di svolgere una funzione completamente nuova? "

Con una funzione di generatore non abbiamo bisogno di riscrivere tanta logica.
print (successivo (find_char (s, p))) # 1

Ora potremmo recuperare il primo valore dell'elenco fornito dalla nostra soluzione originale, ma in questo modo otteniamo solo la prima corrispondenza e smettiamo di scorrere l'elenco. Il generatore verrà quindi scartato e non verrà creato nient'altro; memoria di massa.

Conclusione

Se stai creando una funzione, accumula valori in un elenco come questo.

def foo (bar):
  valori = []
  per x in bar:
    # qualche logica
    values.append (x)
  valori di ritorno

Prendi in considerazione l'idea di restituire un iteratore con una classe, una funzione del generatore o un'espressione del generatore in questo modo:

def foo (bar):
  per x in bar:
    # qualche logica
    resa x

Risorse e fonti

PEP

  • generatori
  • Espressioni del generatore PEP
  • Resa da PEP

Articoli e discussioni

  • iteratori
  • Iterable vs Iterator
  • Documentazione del generatore
  • Iteratori vs generatori
  • Espressione del generatore vs funzione
  • Generatori recrusivi

definizioni

  • iterable
  • Iterator
  • Generatore
  • Generatore Iteratore
  • Espressione del generatore

Originariamente pubblicato su https://blog.dacio.dev/2019/05/03/python-iterators-and-generators/.