Ez a bejegyzés az előző, hasonló című folytatása, ezért annak ismerete nélkül nem biztos, hogy érthető lesz, ezért ha még nem tette, olvassa el azt először.
Most tehát a célunk, hogy olyan függvényt állítsunk elő, amelynek argumentumként tetszőleges iterálható objektum adható át, és ennek adott hosszúságú, egymást követő részsorozatait adja ki. Ebben az esetben se sorozathosszt, se indexelést nem tudunk használni, így a sorozat típusú konténerekre alkalmazható szeletképzés sem jöhet szóba. De van megoldás, nem is egy, de ezekhez az itertools modul islice() és zip_longest() függvényeit fogjuk igénybe venni.
Bármelyik változatnál az első lépés mindenképpen az, hogy előállítjuk az argumentum iterátorát az iter() beépített függvénnyel.
Az islice() egy iterálható objektum által szolgáltatott sorozat egy részsorozatának kinyerésére alkalmas. A célja és használati módja hasonlatos a sorozattípusú konténerek szeletképzéséhez azzal az eltéréssel, hogy egy tetszőleges iterálható objektumból indulunk ki, és az eredmény egy iterátor, amely kérésre a részsorozat elemeit szolgáltatja. Az islice() első argumentuma az iterálandó objektum. Ha ezt követően egyetlen pozitív egész számot adunk meg, az azt jelenti, hogy a részsorozat hány elemből fog állni.
Az egyes függvényváltozatokat a következő kódsorok mutatják. Ezt követően némi magyarázatot is fűzünk mindegyikhez.
|
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 |
from itertools import islice, zip_longest def darabonként5(iterobj, n: int = 1): itr = iter(iterobj) while darab := tuple(islice(itr, n)): yield darab def darabonként6(iterobj, n: int = 1): itr = iter(iterobj) return iter(lambda: tuple(islice(itr, n)), ()) def darabonként7(iterobj, n: int = 1): itr = iter(iterobj) yield from zip_longest(*[itr] * n) # TESZT iterálható_objektum = (x for x in range(11)) print(list(darabonként5(iterálható_objektum, 3))) # Eredmény: [(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10)] iterálható_objektum = (x for x in range(11)) print(list(darabonként6(iterálható_objektum, 3))) # Eredmény: [(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10)] iterálható_objektum = (x for x in range(11)) print(list(darabonként7(iterálható_objektum, 3))) # Eredmény: [(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, None)] |
Az ötödik függvényben láthatjuk, hogy az elérendő célt viszonylag egyszerűen érjük el az islice() ciklusban való használatával. Itt kihasználtuk azt, hogy minden egyes iterációnál az islice() argumentumaként szereplő iterátor kiadott elemeinek a száma legfeljebb n darabbal csökken míg ki nem ürül. Üres iterátor esetén a tuple is üres lesz, aminek az igazságértéke False, és az adja a while-ciklus leállási feltételét.
A hatodik függvényben szintén az islice() függvényt használtuk, de most nem ciklusban, hanem egy lambdafüggvényben, amit az iter() beépített függvénynek adunk át első argumentumként. Ebben az esetben az iter() egy kevésbé közismert hívási lehetőségét használtuk ki. Ha ugyanis az iter()-nek adunk második argumentumot, akkor az első egy hívható objektum kell, hogy legyen. Az iter() visszatérési értékeként létrejött iterátor minden egyes elemkikérésnél meghívja a hívható objektumot. A hívás visszatérési értéke lesz az iterátor által visszaadott elem. Ha a visszatérési érték megegyezik a második argumentumban megadott értékkel, akkor további elemkiszolgáltatás nem lesz. Tehát a hatodik függvény által visszaadott iterátor minden egyes elemkikérésre meghívja a lambdafüggvényt, ami egy legfeljebb n hosszú következő részsorozatot ad vissza egy tuple objektumban. A kikérés következtében – ahogy arról az ötödik függvénynél már szó volt – az islice() argumentumaként szereplő iterátor kiadott elemeinek a száma legfeljebb n darabbal csökken míg ki nem ürül. Üres iterátor esetén a tuple is üres lesz. Mivel az iter() második argumentuma üres tuple, ezért ezt elérve az elemszolgáltatás leáll.
Az utolsó, hetedik függvényben egy teljesen más elven működő megoldást mutatunk. Itt az itertools modul zip_longest() függvényét használjuk. Hasonló a célja és működése a zip() beépített függvényhez azzal az eltéréssel, hogy ha a megadott iterálható objektumok eltérő elemszámot produkálnak, akkor a létrejött iterátor által szolgáltatott elemszám a legnagyobb elemszámú iterálható objektum elemszámával fog megegyezni. A kevesebb elemet szolgáltató iterátorok a hiányzó elemeket a fillvalue paraméternek átadott értékkel fogják pótolni, ami alapértelmezésben None.
A valódi „trükk” a zip_longest() argumentumában van. Itt egy olyan listát látunk, amelynek eleme a függvényünk argumentumában megadott iterálható objektum iterátora. A listán az elemismétlés műveletét hajtjuk végre, mégpedig a kívánt n részsorozathosszal. Ennek eredménye egy olyan lista lesz, amelynek n darab eleme van. Ezt a listát utána kicsomagoljuk, ami azt jelenti, hogy a zip_longest() függvénynek n darab iterálható argumentuma lesz. És a lényeg most jön: a lista elemismétlésének következtében minden egyes elem ugyanaz az iterátorobjektum.
A zip_longest() tehát n darab ugyanazon elemeket szolgáltató iterátorokon működik, ami azt eredményezi, hogy minden egyes kikérésnél egy n hosszú részsorozatot ad vissza, ami után minden iterátorban eggyel csökken az elemszám. A végén, ha valamelyik iterátorobjektum kiürül, azaz rövidebb lesz a többinél, akkor a hiányzó elem helyére None kerül.
Ez a megoldás ott jöhet jól, ahol arra van szükség, hogy minden szolgáltatott részsorozat pontosan n hosszú legyen, még az utolsó is. Ez a korábbi függvényváltozatoknál nem volt biztosított.
Ebből és az előző bejegyzésből remélhetőleg kitűnt, hogy milyen fontos az iterálható objektum, az iterátor és konténer fogalmainak pontos értése. Nem egyszer olvashatók olyan szakmai cikkek, ahol „iterable” objektumot váró függvény vagy osztály implementációja le van szűkítve konténerekre. Ez vagy azt jelenti, hogy a szerző nincs tisztában az iterálható objektum fogalmával, vagy ha igen, de nem jelzi a konténerekre való korlátozást az nem korrekt, és félrevezetheti az olvasót.
Éppen ezért, az iterálható objektum, az iterátor és konténer fogalmai, azok koncepcionális eltérései nagyon részletesen, példákkal és hétköznapi hasonlatokkal szemléltetve vannak elmagyarázva a Python tudásépítés lépésről lépésre című e-könyv elején. Ezek olyan alapvető szerkezetei a Python nyelvnek, amelyek lépten-nyomon előfordulnak és ezért pontos megértésük elengedhetetlen nem csak azért, hogy jól értsünk egy olvasott kódot, hanem hogy megfelelően tudjuk használni a Python által kínált, általában ezekre épülő nyelvi lehetőségeket.
Továbbá, e két bejegyzés megoldásaiban felmerült szeletképzést (slicing), a lista- és egyéb konténerépítő kifejezéseket, generátorkifejezést és -függvényt mint alapvető nyelvi elemeket, a yield és yield from közötti különbséget, valamint az iter(), islice() és zip_longest() függvényeket is érdemes az e-könyvben alaposan tanulmányozni az ott szereplő példákon keresztül is, mert a lehetőségeik bővebbek az itt bemutatottnál és sok feladat megoldásában segíthetnek.