Fantastiset iteraattorit ja niiden tekeminen

Kuva John Matychuk on Unsplash

Ongelma

Oppiessani Make Schoolissa olen nähnyt ikätovereideni kirjoittavan toimintoja, jotka luovat esineiden luetteloita.

s = 'baacabcaab'
p = 'a'
def find_char (merkkijono, merkki):
  indeksit = luettelo ()
  hakemistolle str_char luettelossa (merkkijono):
    jos str_char == merkki:
      indices.append (indeksi)
  tuottoindeksit
tulosta (find_char (s, p)) # [1, 2, 4, 7, 8]

Tämä toteutus toimii, mutta se aiheuttaa muutamia ongelmia:

  • Entä jos haluamme vain ensimmäisen tuloksen; onko meidän tehtävä täysin uusi tehtävä?
  • Entä jos kaikki, mitä teemme, on tuloksen ylittäminen kerran, täytyykö meidän tallentaa kaikki elementit muistiin?

Iteraattorit ovat ihanteellinen ratkaisu näihin ongelmiin. Ne toimivat kuten “laiskoja luetteloita” siinä mielessä, että palauttavat luettelon jokaisesta tuotetusta arvosta ja palauttavat jokaisen elementin kerrallaan.

Iteraattorit palauttavat arvot laiskasti; säästää muistia.

Sukellaan siis oppimaan niistä!

Sisäänrakennetut iteraattorit

Toistimet, jotka ovat useimmiten enumerate () ja zip (). Molemmat näistä laiskoista palauttavat arvoja seuraavalla ().

range () ei kuitenkaan ole iteraattori, vaan ”laiska toistettavissa”. - Selitys

Voimme muuntaa alueen () iteraattoriksi iterillä (), joten teemme sen esimerkkeillemme oppimisen vuoksi.

my_iter = iter (alue (10))
tulosta (seuraava (my_iter)) # 0
tulosta (seuraava (my_iter)) # 1

Jokaisen seuraavan () puhelun yhteydessä saamme seuraavan arvon valikoimastamme; onko järkeä oikein? Jos haluat muuntaa iteraattorin sen luetteloksi, annat sille vain luettelonrakentajan.

my_iter = iter (alue (10))
tulosta (lista (my_iter)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Jos jäljittelemme tätä käyttäytymistä, alamme ymmärtää enemmän iteratorien toimintaa.

my_iter = iter (alue (10))
my_list = list ()
yrittää:
  samalla totta:
    my_list.append (seuraava (my_iter))
paitsi StopIteration:
  kulkea
tulosta (oma_luettelo) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Voit nähdä, että meidän piti kääri se kokeilu saalisilmoitukseen. Se johtuu siitä, että iteraattorit nostavat StopIterationia, kun ne on käytetty loppuun.

Joten jos soitamme seuraavaksi loppuun käytettyyn iteraattoriin, saamme tämän virheen.

seuraava (my_iter) # Korottaa: StopIteration

Iteraattorin tekeminen

Yritetään tehdä iteraattori, joka käyttäytyy kuin alue, jossa on vain stop-argumentti, käyttämällä kolmea yleistä iteraattorityyppiä: Luokat, Generaattoritoiminnot (Tuotto) ja Generaattorilausekkeet

luokka

Vanha tapa luoda iteraattori oli selvästi määritellyn luokan kautta. Jotta objekti olisi iteraattori, sen on toteutettava __iter __ (), joka palauttaa itsensä, ja __sext __ (), joka palauttaa seuraavan arvon.

luokka my_range:
  _virta = -1
  def __init __ (itse, lopeta):
    self._stop = lopeta
  def __iter __ (itse):
    palata itse
  def __seuraava __ (itse):
    itse.virta + = 1
    jos itse.virta> = itse._pysäytä:
      nosta StopIteration
    palauta itse.virta
r = my_range (10)
tulosta (lista (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Se ei ollut liian vaikeaa, mutta valitettavasti meidän on seurattava muuttujia seuraavan () puhelun välillä. Henkilökohtaisesti en pidä kattilalevystä tai siitä, että ajattelen silmukoiden muuttamista, koska se ei ole drop-in-ratkaisu, joten pidän parempana generaattoreita

Tärkein etu on, että voimme lisätä lisätoimintoja, jotka modifioivat sen sisäisiä muuttujia, kuten _stop, tai luoda uusia iteraattoreita.

Luokan iteraattoreilla on haittapuoli kattilalevyn tarpeessa, mutta niillä voi olla lisätoimintoja, jotka muuttavat tilaa.

generaattorit

PEP 255 esitteli ”yksinkertaiset generaattorit” tuotto-avainsanan avulla.

Nykyään generaattorit ovat iteraattoreita, jotka on vain helpompi valmistaa kuin luokkansa vastaavia.

Generaattoritoiminto

Generaattoritoiminnot ovat niitä, joista viime kädessä keskusteltiin kyseisessä PEP: ssä, ja ovat suosikkini iteraattorityyppi, joten aloitetaan siitä.

def my_range (stop):
  indeksi = 0
  samalla kun hakemisto 
r = my_range (10)
tulosta (luettelo (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Näetkö kuinka kauniit ne 4 koodiriviä ovat? Se on hiukan huomattavasti lyhyempi kuin luettelomme toteutus, jotta pääset alkuun!

Generaattori toimii iteraattoreissa, joissa on vähemmän kattilalevyä kuin luokissa, joissa normaali logiikkavirta.

Generaattoritoiminnot ”keskeyttävät” automaattisen suorituksen ja palauttavat määritetyn arvon jokaisella seuraavan puhelulla (). Tämä tarkoittaa, että mitään koodia ei suoriteta ennen ensimmäistä seuraavaa () puhelua.

Tämä tarkoittaa, että virtaus on seuraava:

  1. seuraava () kutsutaan,
  2. Koodi suoritetaan seuraavaan tuottoilmoitukseen saakka.
  3. Tuotto-oikealla oleva arvo palautetaan.
  4. Suoritus on keskeytetty.
  5. Toista 1–5 jokaiselle seuraavalle () puhelulle, kunnes viimeinen koodirivi on osunut.
  6. StopIteration on nostettu.

Generaattoritoiminnot sallivat myös sinun käyttää avainsanan tuottoa, joka tulevaisuus seuraava () kutsuu toiseen toistettavaan, kunnes sanottu toistettavuus on käytetty loppuun.

def tuotti_range ():
  tuotto my_range (10)
tulosta (luettelo (saanut_range ())) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Se ei ollut erityisen monimutkainen esimerkki. Mutta voit tehdä sen jopa rekursiivisesti!

def my_range_recursive (lopeta, nykyinen = 0):
  jos nykyinen> = lopeta:
    palata
  tuottovirta
  tuotto my_range_recursive (stop, nykyinen + 1)
r = my_range_recursive (10)
tulosta (luettelo (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Generaattorin lauseke

Generaattorilausekkeiden avulla voimme luoda iteraattoreita yhtenä rivinä ja ne ovat hyviä, kun meidän ei tarvitse antaa sille ulkoisia toimintoja. Valitettavasti emme voi tehdä uutta my_range -lauseketta lausekkeen avulla, mutta voimme työskennellä iterable-sovelluksissa kuten viimeinen my_range-toiminto.

my_doubled_range_10 = (x * 2 x: lle my_range-tilassa (10))
tulosta (luettelo (my_doubled_range_10)) # 0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Upea asia tässä on, että se tekee seuraavat:

  1. Lista kysyy my_doubled_range_10 seuraavaa arvoa.
  2. my_doubled_range_10 pyytää my_range seuraavaa arvoa.
  3. my_doubled_range_10 palauttaa my_range-arvon kerrottuna 2: lla.
  4. Luettelo lisää arvon itselleen.
  5. 1–5 toista, kunnes my_doubled_range_10 nostaa StopIteration -toimintoa, joka tapahtuu, kun my_range tekee.
  6. Palautetaan luettelo, joka sisältää kaikki arvot, jotka palauttaa my_doubled_range.

Voimme jopa suodattaa generaattorilausekkeilla!

my_even_range_10 = (x x: lle my_range-tilassa (10), jos x% 2 == 0)
tulosta (luettelo (my_even_range_10)) # [0, 2, 4, 6, 8]

Tämä on hyvin samanlainen kuin edellinen paitsi, että my_even_range_10 palauttaa vain arvot, jotka vastaavat annettua ehtoa, joten vain tasoarvot ovat alueella [0, 10).

Kaikessa tässä luomme vain luettelon, koska kerroimme sen.

Hyöty

Lähde

Koska generaattorit ovat iteraattoreita, iteraattorit ovat iterableja, ja iteratorit palauttavat arvot laiskasti. Tämä tarkoittaa, että tämän tiedon avulla voimme luoda esineitä, jotka antavat meille esineitä vain, kun me niitä pyydämme ja vaikka monet haluammekin.

Tämä tarkoittaa, että voimme siirtää generaattorit toimintoihin, jotka vähentävät toisiaan.

tulosta (summa (my_range (10))) # 45

Summan laskeminen tällä tavoin välttää luettelon luomisen, kun kaikki tekemämme on vain lisätä ne yhteen ja heittää pois.

Voimme korvata ensimmäisen esimerkin paremmin generaattoritoiminnolla!

s = 'baacabcaab'
p = 'a'
def find_char (merkkijono, merkki):
  hakemistolle str_char luettelossa (merkkijono):
    jos str_char == merkki:
      tuottoindeksi
tulosta (luettelo (etsi_hakki (s, p))) # # 1, 2, 4, 7, 8]

Nyt heti ei ehkä ole mitään selvää hyötyä, mutta siirrytään ensimmäiseen kysymykseeni: “entä jos haluamme vain ensimmäisen tuloksen; onko meidän tehtävä täysin uusi toiminto? ”

Generaattoritoiminnolla meidän ei tarvitse kirjoittaa niin paljon logiikkaa.
tulosta (seuraava (find_char (s, p)))) # 1

Nyt voimme hakea alkuperäisen ratkaisumme antaman luettelon ensimmäisen arvon, mutta tällä tavalla saamme vain ensimmäisen ottelun ja lopettaa iteraation luettelon yli. Generaattori hävitetään sitten eikä mitään muuta luoda; säästää huomattavasti muistia.

johtopäätös

Jos olet koskaan luomassa toimintoa, kertyvät arvot tällaisessa luettelossa.

def foo (baari):
  arvot = []
  x: lle palkissa:
    # jotain logiikkaa
    values.append (x)
  palauta arvot

Harkitse sen palauttamista iteraattoriksi, jolla on luokka, generaattorifunktio tai generaattorilauseke kuten:

def foo (baari):
  x: lle palkissa:
    # jotain logiikkaa
    saanto x

Resurssit ja lähteet

Poliittisesti

  • generaattorit
  • Generaattorin lausekkeet PEP
  • Tuotto PEP: stä

Artikkelit ja säikeet

  • iterators
  • Kunnioittamaton vs.
  • Generaattorin dokumentaatio
  • Iteraattorit vs. generaattorit
  • Generaattorin lauseke vs. toiminto
  • Rekrytoivat generaattorit

Määritelmät

  • Iterable
  • Iterator
  • Generaattori
  • Generaattori
  • Generaattorin lauseke

Alun perin julkaistu osoitteessa https://blog.dacio.dev/2019/05/03/python-iterators-and-generators/.