Fantastiske Iteratorer og hvordan man gør dem

Foto af John Matychuk på Unsplash

Problemet

Mens jeg lærte på Make School, har jeg set mine kammerater skrive funktioner, der opretter lister over emner.

s = 'baacabcaab'
p = 'a'
def find_char (streng, karakter):
  indekser = liste ()
  for indeks, str_char i enumerate (streng):
    hvis str_char == karakter:
      indices.append (indeks)
  returindeks
udskriv (find_char (s, p)) # [1, 2, 4, 7, 8]

Denne implementering fungerer, men den giver et par problemer:

  • Hvad hvis vi kun ønsker det første resultat; bliver vi nødt til at gøre en helt ny funktion?
  • Hvad hvis alt, hvad vi gør, er at slå resultatet over én gang, skal vi gemme hvert element i hukommelsen?

Iteratorer er den ideelle løsning på disse problemer. De fungerer som ”doble lister”, idet de i stedet for at returnere en liste med hver værdi, den producerer, og returnerer hvert element et ad gangen.

Iteratorer vender tilbage dovne værdier; gemme hukommelse.

Så lad os dykke ned i at lære om dem!

Indbyggede Iteratorer

De iteratorer, der oftest er, er tæller () og zip (). Begge disse dovne returnerer værdier ved næste () med dem.

rækkevidde () er imidlertid ikke en iterator, men en "doven iterable." - Forklaring

Vi kan konvertere rækkevidde () til en iterator med iter (), så vi gør det for vores eksempler af hensyn til læring.

my_iter = iter (rækkevidde (10))
print (næste (my_iter)) # 0
print (næste (my_iter)) # 1

Ved hvert opkald fra næste () får vi den næste værdi i vores interval; giver mening ret? Hvis du vil konvertere en iterator den til en liste, skal du bare give den listekonstruktøren.

my_iter = iter (rækkevidde (10))
udskriv (liste (my_iter)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Hvis vi efterligner denne opførsel, begynder vi at forstå mere om, hvordan iteratorer fungerer.

my_iter = iter (rækkevidde (10))
my_list = liste ()
prøve:
  mens sandt:
    my_list.append (næste (my_iter))
undtagen StopIteration:
  passere
print (min_liste) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Du kan se, at vi havde brug for at pakke det ind i en prøve-fangstopgørelse. Det er fordi iteratorer hæver StopIteration, når de er opbrugt.

Så hvis vi næste kalder på vores udmattede rækkevidde iterator, får vi den fejl.

næste (my_iter) # Hæver: StopIteration

At lave en Iterator

Lad os prøve at lave en iterator, der opfører sig som rækkevidde med kun stop-argumentet ved at bruge tre almindelige typer iteratorer: Klasser, Generatorfunktioner (Yield) og Generator Expressions

klasse

Den gamle måde at skabe en iterator på var gennem en eksplicit defineret klasse. For at et objekt skal være en iterator, skal det implementere __iter __ (), der returnerer sig selv og __næste __ (), der returnerer den næste værdi.

klasse my_range:
  _strøm = -1
  def __init __ (self, stop):
    self._stop = stop
  def __iter __ (selv):
    vende tilbage selv
  def __næste __ (selv):
    selv._strøm + = 1
    hvis self._current> = self._stop:
      hæv StopIteration
    vende tilbage selv
r = my_range (10)
udskriv (liste (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Det var ikke for svært, men desværre er vi nødt til at holde styr på variabler mellem opkald til næste (). Personligt kan jeg ikke lide kedelpladen eller ændre, hvordan jeg tænker på sløjfer, fordi det ikke er en drop-in-løsning, så jeg foretrækker generatorer

Den største fordel er, at vi kan tilføje yderligere funktioner, der ændrer dets interne variabler såsom _stop eller oprette nye iteratorer.

Klasse iteratorer har ulempen med at have brug for kedelplade, men de kan have yderligere funktioner, der ændrer tilstand.

generatorer

PEP 255 introducerede “enkle generatorer” ved hjælp af udbyttetøgleordet.

I dag er generatorer iteratorer, der bare er lettere at lave end deres klassekontrakter.

Generatorfunktion

Generatorfunktioner er det, der i sidste ende blev diskuteret i den PEP og er min foretrukne type iterator, så lad os starte med det.

def my_range (stop):
  indeks = 0
  mens indeks 
r = my_range (10)
udskriv (liste (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Ser du, hvor smukke disse 4 linjer med kode er? Det er lidt markant kortere end vores listeimplementering for at top!

Generator fungerer iteratorer med mindre kedelplade end klasser med en normal logisk strøm.

Generator fungerer automatisk "pause" udførelse og returnerer den specificerede værdi ved hvert opkald fra næste (). Dette betyder, at der ikke køres nogen kode, før det første næste () opkald.

Dette betyder, at flowet er sådan:

  1. næste () kaldes,
  2. Koden udføres op til den næste afkastopgørelse.
  3. Værdien på udbytteretten returneres.
  4. Udførelse er sat på pause.
  5. Gentag 1–5 for hvert næste () opkald, indtil den sidste kodelinje er ramt.
  6. StopIteration hæves.

Generatorfunktioner giver dig også mulighed for at bruge udbyttet fra nøgleord, som fremtiden næste () kalder til en anden iterable, indtil nævnte iterable er opbrugt.

def yielded_range ():
  udbytte fra my_range (10)
print (liste (udbyttet_range ())) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Det var ikke et særligt komplekst eksempel. Men du kan endda gøre det rekursivt!

def my_range_recursive (stop, nuværende = 0):
  hvis nuværende> = stop:
    Vend tilbage
  udbytte strøm
  udbytte fra my_range_recursive (stop, nuværende + 1)
r = my_range_recursive (10)
udskriv (liste (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Generator Expression

Generatorudtryk giver os mulighed for at oprette iteratorer som enlinjer og er gode, når vi ikke behøver at give det eksterne funktioner. Desværre kan vi ikke lave en ny my_range ved hjælp af et udtryk, men vi kan arbejde på iterables som vores sidste my_range-funktion.

my_doubled_range_10 = (x * 2 for x i my_range (10))
print (liste (my_doubled_range_10)) # 0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Den seje ting ved dette er, at det gør følgende:

  1. Listen spørger min_doubled_range_10 om dens næste værdi.
  2. my_doubled_range_10 spørger min_range om dens næste værdi.
  3. my_doubled_range_10 returnerer my_ranges værdi ganget med 2.
  4. Listen tilføjer værdien til sig selv.
  5. 1–5 gentag, indtil my_doubled_range_10 hæver StopIteration, der sker, når my_range gør det.
  6. Listen returneres med hver værdi returneret af my_doubled_range.

Vi kan endda filtrere ved hjælp af generatorudtryk!

my_even_range_10 = (x for x i my_range (10) hvis x% 2 == 0)
print (liste (my_even_range_10)) # [0, 2, 4, 6, 8]

Dette ligner meget den foregående, bortset fra at my_even_range_10 kun returnerer værdier, der matcher den givne betingelse, så kun jævne værdier mellem i området [0, 10).

I løbet af alt dette opretter vi kun en liste, fordi vi fortalte det til.

Fordelen

Kilde

Fordi generatorer er iteratorer, er iteratorer iterables, og iteratorer vender tilbage doven. Det betyder, at vi ved hjælp af denne viden kan skabe objekter, der kun giver os objekter, når vi beder om dem, og hvor mange vi end kan lide.

Dette betyder, at vi kan videregive generatorer til funktioner, der reducerer hinanden.

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

Ved at beregne summen på denne måde undgår man at oprette en liste, når alt, hvad vi gør, er at tilføje dem sammen og derefter kassere.

Vi kan omskrive det allerførste eksempel for at blive meget bedre ved hjælp af en generatorfunktion!

s = 'baacabcaab'
p = 'a'
def find_char (streng, karakter):
  for indeks, str_char i enumerate (streng):
    hvis str_char == karakter:
      udbytteindeks
udskriv (liste (find_char (s, p))) # [1, 2, 4, 7, 8]

Nu er der muligvis ikke nogen åbenlyst fordel, men lad os gå til mit første spørgsmål: ”hvad nu hvis vi kun vil have det første resultat; bliver vi nødt til at lave en helt ny funktion? ”

Med en generatorfunktion behøver vi ikke at omskrive så meget logik.
udskriv (næste (find_char (s, p))) # 1

Nu kunne vi hente den første værdi på listen, som vores oprindelige løsning gav, men på denne måde får vi kun den første kamp og stopper med at iterere over listen. Generatoren kasseres derefter, og der oprettes intet andet; massivt gemme hukommelse.

Konklusion

Hvis du nogensinde opretter en funktion, akkumuleres værdier på en liste som denne.

def foo (bar):
  værdier = []
  for x i bjælken:
    # noget logik
    values.append (x)
  return værdier

Overvej at få det til at returnere en iterator med en klasse, generatorfunktion eller generatorudtryk som sådan:

def foo (bar):
  for x i bjælken:
    # noget logik
    udbytte x

Ressourcer og kilder

PEPs

  • generatorer
  • Generatorudtryk PEP
  • Udbytte fra PEP

Artikler og tråde

  • iteratorer
  • Iterable vs Iterator
  • Generator Dokumentation
  • Iteratorer vs generatorer
  • Generatorudtryk vs funktion
  • Rekrusive generatorer

Definitioner

  • Iterable
  • iterator
  • Generator
  • Generator Iterator
  • Generator Expression

Oprindeligt offentliggjort på https://blog.dacio.dev/2019/05/03/python-iterators-and-generators/.