Ebben a bejegyzésben az iterátorok és generátorok (generátorkifejezés és -függvény) készítését és használatát fogjuk gyakorolni a címben feltett kérdésre válaszolva.
A konténer- és iterálható objektum, valamint az iterátor és generátor fogalmainak és főbb jellemzőinek ismerete szükséges a továbbiak megértéséhez. Ha bizonytalan vagy e téren, akkor érdemes az ezeket lényegre törően összefoglaló „Hogyan ellenőriznéd, hogy iterálható-e egy objektum?” és „Mi az iterátor, generátor, generátor-iterátor és iterátor generátor, generátorfüggvény és -kifejezés?” című korábbi bejegyzéseket újra elolvasni, vagy ha megvan, a Python tudásépítés lépésről lépésre című e-könyvben ezeknek utánanézni (releváns fejezetek a bejegyzés végén sorolva), ahol leírásuk, jellemzőik és használatuk nagyon részletesen ki van fejtve.
Nos, tehát a feladatunk az, hogy készítsünk egy kétparaméteres függvényt, amelynek első iterobj nevű paramétere egy iterálható objektumot fogad. A másik, mondjuk int_iterobj nevű paraméter pedig egy olyan iterálható objektumot fogad, amelyből int típusú, nemnegatív egész számok kérhetők ki. A függvény egy olyan iterátort ad vissza, amely az iterobj elemeiből csak azon pozíciójú elemeket szolgáltatja, amelyek sorszámai, sorrendpozíciói mint egész számok az int_iterobj objektumból kinyerhetők.
Vegyük észre, hogy az első argumentumról nincs más információ, minthogy iterálható. Tehát nem feltételezhetjük, hogy sorozat típusú konténer, amelyből simán indexeléssel ki lehetne venni az elemeket. Ezért is fogalmaztunk úgy a második argumentum esetén, hogy az sorszámot, pozíciót meghatározó egészeket szolgálatat. Mivel tehát nem a sorozatkonténereknél használható indexszámokról van szó, ezért ezen egész számok nem lehetnek negatívak. Ezzel együtt, a rövidebb és könnyebb hivatkozás okán egyszerűen indexeknek fogjuk hívni a továbbiakban a második argumentum által biztosított egész számokat. Annak oka pedig, hogy a függvényünk nem valamilyen konténerrel (pl. list vagy tuple), hanem iterátorral tér vissza az, hogy egyik argumentumra sem kötöttük ki, hogy véges számú értéket szolgáltat.
Bemelegítésképpen egy pillanatra mégis tegyük fel, hogy a kiszűrendő elemek indexei véges számúak, és egy sorozat típusú konténerben adhatók meg mind második argumentum. Ebben az esetben a függvényünk elég egyszerű felépítésű lesz. Annyit kell csak tenni, hogy
1) az iterobj minden egyes kikért eleméhez egy sorszámot rendelünk 0 kezdőértéktől indulva. Ezt legegyszerűbben az enumerate() beépített függvénnyel érhetjük el.
2) Az így előálló iterátorból egy generátorkifejezésben sorban egymás után kikérjük az index-elem párokat, és a generátor csak azokat az elemeket adja ki, amelyekhez tartozó index a második argumentumban szerepel.
3) E generátorkifejezés lesz a függvény visszatérési értéke.
E függvény definícióját láthatjuk alább. A teszthívásoknál a könnyebb követhetőség érdekében az iterobj 0 kezdőértékű egészeket szolgáltat, vagyis a meghatározott, véges számú indexek értékével azonos értékeket ad vissza a függvény minden egyes iterációkor.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from typing import Container, Iterable def filter_by_finite_number_of_indexes(iterobj: Iterable, indexes_iterobj: Container): """Egy olyan iterátort ad vissza, amely az 'iterobj' iterálható objektum elemeiből csak azon pozíciójú elemeket szolgáltatja, amelyek pozícióindexei az 'indexes_iterobj' paraméternek átadott konténerben szerepelnek.""" return (elem for index, elem in enumerate(iterobj) if index in indexes_iterobj) # TESZT print(*filter_by_finite_number_of_indexes((x for x in range(100)), (1, 4, 9, 16))) print(*filter_by_finite_number_of_indexes((x for x in range(100)), [j ** 2 for j in range(1, 5)])) # Eredmények: # 1 4 9 16 # 1 4 9 16 |
Nem ilyen egyszerű a helyzet, ha nem csak konténerben adhatjuk meg a szűrési feltételként szolgáló indexeket, hanem egy tetszőleges iterálható objektumban. Itt a lépések a következők:
1) Hasonlóan, mint előbb, az iterobj minden egyes kikért eleméhez egy sorszámot rendelünk 0 kezdőértéktől az enumerate() függvénnyel.
2) Előállítjuk az indexeket szolgáltató iterálható objektum iterátorát.
3) Kikérjük a következő szűrési feltételt meghatározó indexet.
4) Egy ciklusban sorban egymás után kikérjük a sorszám-elem párokat az 1) pontban kapott iterátorból mindaddig, amíg a sorszám nem egyezik meg a szűrési indexszel.
5) Ha sorszám megegyezik a szűrési indexszel, akkor a sorszámhoz tartozó elemet kiadjuk egy yield utasítással, majd kilépünk a ciklusból, hogy a 3) pontra visszamenve vehessük a következő szűrőindexet.
6) Ha valamelyik argumentum nem tud már több elemet szolgáltatni, akkor StopIteration kivételt fog dobni. Ezért az eddigi lépések ciklusait és utasításait egy try…except… kivételkezelő szerkezetbe foglaljuk. A StopIteration kivételt lekezelve végleg kilépünk a generátorfüggvényből, hiszen több elem szolgáltatása már nem várható.
Az ennek megfelelő, filter_by_index_sequence nevű függvénydefiníciója az alábbi:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
def filter_by_index_sequence(iterobj: Iterable, indexes_iterobj: Iterable): """Egy olyan iterátort ad vissza, amely az 'iterobj' iterálható objektum elemeiből csak azon pozíciójú elemeket adja ki, amelyek monoton növekvő pozícióindexeit az 'indexes_iterobj' iterálható objektum szolgáltatja.""" # Az 'iterobj' minden egyes kikért eleméhez egy sorszámot rendelünk 0 kezdőértéktől indulva. index_item_pairs = enumerate(iterobj) # Előállítjuk az indexeket szolgáltató iterálható objektum iterátorát. indexes_iterator = iter(indexes_iterobj) try: while True: # Kikérjük a következő szűrési feltételt meghatározó indexet. i = next(indexes_iterator) # Addig vesszük sorban ki a sorszám-elem párokat, amíg a sorszám nem egyezik meg a szűrési indexszel. while True: index, item = next(index_item_pairs) # Ha sorszám azonos a szűrési indexszel, akkor a sorszámhoz tartozó elemet kiadjuk, és # kilépünk a ciklusból, hogy vehessük a következő szűrőindexet. if i == index: yield item break # Ha valamelyik argumentum nem tud már több elemet szolgáltatni, akkor StopIteration kivételt dob. # Ezt lekezelve kilépünk a generátorfüggvényből. except StopIteration: pass |
Ahhoz, hogy ne csak konténerrel tudjuk tesztelni e függvényt, kellene egy olyan iterálható objektum, amely az indexeket szolgáltatja. Ráadásul nem lenne baj, ha ez olyan lenne, amelyben az indexértékek egy megadható függvény szerint képződnének, mint ahogy láttuk a korábbi tesztsorokban a listaépítő kifejezésben.
Mivel a szűrési feltételt meghatározó indexek nem biztos, hogy véges számúak, ezért itt is egy generátorfüggvényt fogunk definiálni, amelynek első argumentumként azt a függvényobjektumot adjuk meg, amelyet nemnegatív egészekből álló sorozat elemeire meghívva előáll a kívánt indexek sorozata. Az ennek megfelelő függvénytörzs alaplogikája elég egyszerű. Ebben az itertools modul count() függvényével 0 kezdőértékkel sorszámokat generálunk. Minden egyes kikért sorszámra meghívjuk az argumentumban megadott függvényt, aminek eredményét egy yield utasítással adjuk vissza a hívó programnak. Ez így végtelen sok indexértéket fog kiadni.
Ez azonban nem biztos, hogy minden használati esetben megfelelő, mert lehet, hogy be szeretnénk korlátozni a kiadható indexeket egy bizonyos kezdő értéktől egy megadható végértékig. Ez az igény csak egy kicsit bonyolítja a függvénytörzset, mert csak annyit kell tenni, hogy az indexértéket két feltétel teljesülése esetén adjuk ki a yield utasítással. Az ezen követelményeknek megfelelő index_gen() nevű függvény definícióját láthatjuk alább két megvalósításban. Az elsőben az előbb leírtakat követve ciklust és feltételes elágazásokat használtunk, míg a második változatban a map() beépített függvényen kívül bevetettük az itertools modul arzenáljából a dropwhile() és takewhile() függvényeket, és így a ciklusszerkezetre nincs szükség.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
from itertools import count, dropwhile, takewhile def index_gen(fn, start=0, stop=None): """Az fn monoton növekvő függvényérték sorozatát szolgáltató iterátort ad vissza a start értéktől addig, amíg a visszaadott érték kisebb, mint a stop. Ha nem adunk meg stop értéket, akkor a visszadott iterátor mindig fog új elemet szolgáltatni""" for i in count(): if (index := fn(i)) >= start: if stop is not None and index >= stop: return yield index def index_gen(fn, start=0, stop=None): """Az fn monoton növekvő függvényérték sorozatát szolgáltató iterátort ad vissza a start értéktől addig, amíg a visszaadott érték kisebb, mint a stop. Ha nem adunk meg stop értéket, akkor a visszadott iterátor mindig fog új elemet szolgáltatni""" indexes1 = map(fn, dropwhile(lambda n: fn(n) < start, count())) if stop is not None: indexes2 = takewhile(lambda k: k < stop, indexes1) return indexes2 return indexes1 |
A működést a következő tesztsorokkal ellenőrizzük, amelyekből az egyik esetben az indexeket sorozat típusú konténerből vesszük, a másik két esetben az index_gen() függvényt alkalmaztuk egyik esetben végtelen indexérték kiadására, másik esetben lekorlátoztuk véges számú szűrőindex szolgáltatására.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# TESZT # Az indexeket konténer tartalmazza. print(*filter_by_index_sequence((x for x in range(100)), [1, 4, 9, 16])) # Az indexeket generátorfüggvénnyel állítjuk elő, adott kezőértéktől. print(*filter_by_index_sequence((x for x in range(100)), index_gen(lambda n: n ** 2, 1))) # Az indexeket generátorfüggvénnyel állítjuk elő, adott kezőértéktől és végértékig. print(*filter_by_index_sequence((x for x in range(100)), index_gen(lambda n: n ** 2, start=1, stop=40))) # Eredmények: # 1 4 9 16 # 1 4 9 16 25 36 49 64 81 # 1 4 9 16 25 36 |
E bejegyzés tartalmához kapcsolódóan további részletes információkat a Python tudásépítés lépésről lépésre című e-könyv következő részeiben találunk: „Iterátorok és elemeik kinyerésének nyelvi megvalósítása” fejezet, „Műveletek” fejezet – „Különleges műveletek” alfejezet – „Iterátorépítés – generátor kifejezés” alcím, valamint „Kifogyhatatlan sorozatlövők – generátorfüggvények” fejezet, valamint a „Készétel fogyasztás – a szabványos könyvtár moduljainak használata” fejezet „ Speciális iterátorok” alfejezete.