A szabványos könyvtár itertools moduljának takewhile() iterátora a második argumentumként megadott sorozat elemeit mindaddig kiadja, amíg az első argumentumként átadott függvény az elemekre meghívva True értéket eredményez. A takewhile() használatakor azonban megfontoltnak kell lenni; át kell gondolni, hogy az adott helyzetben alkalmazható-e. Ugyanis, amennyiben a sorozatelemeket szolgáltató argumentum egy iterátor, az az elem, amely először nem felel meg a feltételnek, kikerül a bemeneti sorozatból, és később már nem érhető el. Ez gondot okozhat, ha a takewhile() futása után a maradék sorozatelemeket is fel kell dolgozni.
Ha tehát a feladat olyan, hogy egy sorozatot két részre kell bontani egy feltétel alapján és utána mindkét sorozatrészt fel kell dolgozni, akkor ennek megvalósítására a takewhile() nem alkalmazható. Ezért készítsünk magunknak egy olyan iterátort, amely ezt az igényt teljesíteni tudja.
A probléma ugyebár az, hogy ahhoz, hogy eldöntsük, hogy az aktuális elemre a feltétel teljesül, azt ki kell kérni a sorozatértékeket szolgáltató iterátorból. Ha a feltétel nem teljesül, akkor ezt az elemet nem adjuk ki. Viszont ezt valamilyen módon meg kell őrizni ahhoz, hogy ezen értéket is magába foglaló maradék sorozat teljes egészében rendelkezésre álljon.
Mint általában a legtöbb feladatot, ezt is többféle módon lehet megoldani. A most alkalmazott megvalósítás elve a következő:
- A sorozatelemeket szolgáltató iterálható objektumból két független iterátort készítünk az itertools tee iterátorát használva.
- Az első iterátorból sorban kikérjük az értékeket.
- Ha a leállási feltételt meghatározó predikátum függvény az aktuális sorozatelemre True értékű, akkor ezt az értéket kiadjuk, a második iterátorból pedig eltávolítjuk.
- Ha a predikátum függvény False értéket ad vissza, vagyis innentől le kell állni az elemkiadással, akkor egy StopIteration kivételt keltünk.
- Ezt a kivételt lekezeljük úgy, hogy az objektumunk remainder_iterator attribútumához a második iterátort rendeljük (ami a maradék elemeket tudja majd kiadni), és aztán visszatérünk a hívó kódba.
- Ha az első iterátor kiürül, akkor automatikusan egy StopIteration kivétel fog keletkezni, amit az előbbiek szerint, ugyanúgy kezelünk le.
Az igényeinket kielégítő és az előbbi elveknek megfelelő iterátort mind generátorfüggvénnyel, mind iterálható osztálypéldánnyal meg tudjuk valósítani. Ezek definíciói alább láthatók. A kommentek segítik a működés megértését.
|
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# Python 3.10+ from itertools import takewhile, tee from typing import Callable, Any, Iterable def take_while(predicate: Callable[[Any], bool], iterable: Iterable): """A meghívás után létrejött generátor a második argumentumként megadott iterálható objektum elemeit mindaddig kiadja, amíg az első argumentumként átadott függvény az elemekre meghívva True értéket eredményez. Amikor ez False lesz, akkor onnantól kezdve a generátor leáll és nem szolgáltat elemet. Ekkor a generátorobjektum remainder_iterator nevű attribútuma fogja tartalmazni a maradék elemeket szolgáltató iterátort. """ # Az argumentumként kapott iterálható objektumból két független iterátort készítünk. iterator1, iterator2 = tee(iterable) # Az első iterátorból sorban kikérjük az értékeket. Ha a predikátum függvény az # aktuális értékre True értékű, akkor ezt az értéket kiadjuk, majd a második iterátorból eltávolítjuk # ezt az elemet. Ha a predikátum függvény False értéket ad vissza, akkor egy StopIteration # kivételt keltünk. Ezt lekezeljük úgy, hogy a generátor remainder_iterator attribútumához a második # iterátort rendeljük (ami a maradék elemeket tudja kiadni), és aztán visszatérünk a hívó kódba. # Ha az első iterátor kiürül, akkor szintén egy StopIteration kivétel fog keletkezni, amit ugyanúgy kezelünk le. while True: try: item = next(iterator1) if predicate(item): yield item next(iterator2) else: raise StopIteration except StopIteration: take_while.remainder_iterator = iterator2 return class TakeWhile: """Az osztály példánya egy olyan iterátor, amely a példányosításkor második argumentumként megadott iterálható objektum elemeit mindaddig kiadja, amíg az első argumentumként átadott függvény az elemekre meghívva True értéket eredményez. Amikor ez False lesz, akkor onnantól kezdve az értékszolgáltatás leáll. A példány remainder_iterator nevű attribútuma fogja tartalmazni a maradék elemeket szolgáltató iterátort. """ def __init__(self, predicate: Callable[[Any], bool], iterable: Iterable): self.iterator1, self.iterator2 = tee(iterable) self.predicate = predicate def __iter__(self): while True: try: item = next(self.iterator1) if self.predicate(item): yield item next(self.iterator2) else: raise StopIteration except StopIteration: self.remainder_iterator = self.iterator2 return def __next__(self): return next(self) |
A tesztsorok pedig következők.
|
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# TESZT # Generátorfüggvény list_iterator = iter([1, 2, 4, 6, 12, 28, 50]) print([*takewhile(lambda x: x < 12, list_iterator)], ' maradék:', [*list_iterator]) list_iterator = iter([1, 2, 4, 6, 12, 28, 50]) print([*take_while(lambda x: x < 12, list_iterator)], ' maradék:', [*take_while.remainder_iterator]) # Eredmény: # [1, 2, 4, 6] maradék: [28, 50] # [1, 2, 4, 6] maradék: [12, 28, 50] list_iterator = iter([1, 2, 4, 6, 12, 28, 50]) print([*takewhile(lambda x: x < 1, list_iterator)], ' maradék:', [*list_iterator]) list_iterator = iter([1, 2, 4, 6, 12, 28, 50]) print([*take_while(lambda x: x < 1, list_iterator)], ' maradék:', [*take_while.remainder_iterator]) # Eredmény: # [] maradék: [2, 4, 6, 12, 28, 50] # [] maradék: [1, 2, 4, 6, 12, 28, 50] list_iterator = iter([1, 2, 4, 6, 12, 28, 50]) print([*takewhile(lambda x: x < 100, list_iterator)], ' maradék:', [*list_iterator]) list_iterator = iter([1, 2, 4, 6, 12, 28, 50]) print([*take_while(lambda x: x < 100, list_iterator)], ' maradék:', [*take_while.remainder_iterator]) # Eredmény: # [1, 2, 4, 6, 12, 28, 50] maradék: [] # [1, 2, 4, 6, 12, 28, 50] maradék: [] # Iterátor osztálypéldány list_iterator = iter([1, 2, 4, 6, 12, 28, 50]) take_while_instance = TakeWhile(lambda x: x < 12, list_iterator) print([*take_while_instance], ' maradék:', [*take_while_instance.remainder_iterator]) # Eredmény: [1, 2, 4, 6] maradék: [12, 28, 50] list_iterator = iter([1, 2, 4, 6, 12, 28, 50]) take_while_instance = TakeWhile(lambda x: x < 1, list_iterator) print([*take_while_instance], ' maradék:', [*take_while_instance.remainder_iterator]) # Eredmény: [] maradék: [1, 2, 4, 6, 12, 28, 50] list_iterator = iter([1, 2, 4, 6, 12, 28, 50]) take_while_instance = TakeWhile(lambda x: x < 100, list_iterator) print([*take_while_instance], ' maradék:', [*take_while_instance.remainder_iterator]) # Eredmény: [1, 2, 4, 6, 12, 28, 50] maradék: [] |
Bár az itertools modulban található tee iterátor alkalmazása nem túl gyakori, de most egy olyan példát mutattunk, ahol jól tudtuk használni. Ehhez kapcsolódóan megjegyezzük, hogy a tee használata olyan esetekben hatékony, ahol az általa létrehozott iterátorokból felváltva kérjük ki az elemeket, nagyjából egyenlő kis adagokban. Ellenkező esetben az átmeneti memóriahasználat nagy lehet. Ilyenkor egy lista alkalmazása egyszerűbb és gyorsabb futást eredményez.
E bejegyzésben elsődlegesen az iterátorok, generátorfüggvény és iterátor protokoll, valamint az itertools modul takewhile és tee iterátorainak alkalmazása volt a középpontban. Ezekkel a Python tudásépítés lépésről lépésre című e-könyv „Kifogyhatatlan sorozatlövők – generátorfüggvények” és „Mik azok a protokollok?” fejezetek, továbbá a „Mágikus metódusok egyedi igényre szabott osztályokban” fejezet „Iterátorok és iterálható objektumok megvalósítása” alfejezete, valamint a „Készétel fogyasztás – a szabványos könyvtár moduljainak használata” fejezet „Speciális iterátorok” alfejezete foglalkozik részletesen.