Az előző bejegyzésben sorozat típusú konténer egyéni igényekre szabott osztályát definiáltuk, amely a sorozat típusú konténerek alapvető kritériumait teljesíti. Most ezt az osztályt fejlesztjük tovább úgy, hogy képes legyen a szeletképzésre (slicing), valamint egy másik, ugyanilyen típusú sorozatkonténerrel való összefűzésre (concatenation).
Ahogy az előző bejegyzésben említettük, a szeletképzés képességét a __getitem__ metódus megfelelő implementálásával lehet elérni, mert amikor egy sorozatobjektumra alkalmazzuk a szeletképzés operátorát (pl. [1:10:2]), akkor az interpreter az objektumra meghívja a __getitem__ metódust egy olyan szeletobjektummal (slice object) mint argumentummal, amelynek a jellemzői az operátorban megadottak. Tehát a __getitem__ metódus nem csak int típusú indexértéket, hanem slice típusú szeletobjektumot is fogadhat, amiből következik, hogy a metódustörzsben az argumentum típusa szerint más-más kódsoroknak kell lefutniuk, vagyis első lépésben típusellenőrzést kell végezni. Ha ennek eredménye az, hogy a __getitem__ argumentuma int típusú, akkor az előző bejegyzésben látott kódsort kell végrehajtani. Most azt a kódblokkot fogjuk megalkotni, ami akkor kell, hogy lefusson, ha a __getitem__ argumentuma slice típusú. Ha az argumentum se nem int, se nem slice típusú, akkor TypeError kivételt dobunk egy megfelelő hibaüzenettel.
Ha a __getitem__ argumentuma – ami jelen esetben index névre hallgat – slice típusú, akkor a szeletobjektum három jellemzőjét (kezdőindex, végindex és lépésköz) az index.start, index.stop és index.step attribútumhivatkozásokkal tudjuk kikérni. Ezzel a feladat, vagyis a szeletképzés, látszólag egyszerűen megoldható, mert ezen értékekkel egy range objektumot hozunk létre, és az általa szolgáltatott minden egyes indexértékkel a sorozatpéldányt indexeljük egy generátorkifejezésben, amit pedig a Sorozat konstruktorkifejezésébe helyezünk. Végül ezt fogja visszaadni a __getitem__ metódus. Leírva kicsit bonyolultnak hangzik, de kódban világosabb miről van szó a megoldási ötletünkben:
|
1 2 3 4 5 |
k, v, n = index.start, index.stop, index.step return type(self)(self[i] for i in range(k, v, n)) |
Bár a megoldás elve jó, de ebben a formában sajnos több okból sem megfelelő. Az egyik, hogy a szeletobjektum három attribútumának bármelyik értéke lehet None. Viszont a range() egyik paramétere sem kaphat None értéket, csak int számot. Ha egy szeletobjektum valamely jellemzője None, akkor az azt jelenti, hogy ott az alapértelmezett értéket kell venni. Tehát erről a kódunkban mindenképp gondoskodnunk kell.
A másik gond, hogy a szeletindexek lehetnek negatívak és pozitívak is, és ilyenkor értelmezett indexsorozat nem mindig fog megegyezni az ugyanilyen kezdőindexszel, végindexszel és lépésközzel definiált range által szolgáltatott indexsorozattal. Például egy 8 elemű sorozat esetén a [-9:3:1] szelet 3 egymást követő indexet határoz meg, míg a range(-9,3,1) 12 darabot. Továbbá a [2:-9:-1] szelet szintén 3 egymást követő indexet határoz meg, míg a range(2,-9,-1) 11 darabot.
Ezért tehát a szeletobjektum attribútumértékeit konvertálni kell a megfelelő range paraméterértékre. Ennek elve az lesz, hogy attól függően, hogy a lépésköz milyen előjelű, a range kezdő- és végindexei is ugyanilyen előjelűek lesznek. Vagyis, ha a lépésköz pozitív, azaz növekvő indexsorozat esetén a kezdő és végindex is pozitív szám lesz, és fordítva, ha a lépésköz negatív, azaz csökkenő indexsorozat esetén a kezdő és végindex is negatív szám lesz. Ez az előbbi példánál azt jeleneti, hogy [-9:3:1] szelet esetén a range kezdőindexe 0, végindexe 3 és növekménye 1 lesz, illetve [2:-9:-1] szelet esetén a range kezdőindexe -6, végindexe -9 és a lépésköz -1 lesz.
A fenti megfontolások alapján a Sorozat osztály __getitem__ metódusának teljes kódját láthatjuk alább. /Az előző bejegyzésben kidolgozott metódusok törzsét helytakarékosság okán és a jobb áttekinthetőség érdekében nem tüntettük fel. /
|
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 |
class Sorozat: def __init__(self, iterobj):... def __repr__(self):... def __str__(self):... def __contains__(self, elem):... def __len__(self):... def __reversed__(self):... def __getitem__(self, index): if isinstance(index, int): i = index if index >= 0 else len(self.indexek_elemek) + index if not 0 <= i < len(self.indexek_elemek): raise IndexError return self.indexek_elemek[i] elif isinstance(index, slice): n = index.step if index.step is not None else 1 if n >= 0: k = (index.start if index.start >= 0 else len(self) + index.start) if index.start is not None else 0 v = (index.stop if index.stop >= 0 else len(self) + index.stop) if index.stop is not None else len(self) else: k = (index.start - len(self) if index.start >= 0 else index.start) if index.start is not None else -1 v = (index.stop - len(self) if index.stop >= 0 else index.stop) if index.stop is not None else -len( self) - 1 return type(self)(self[i] for i in range(k, v, n)) else: raise TypeError('Az argumentum típusa csak int vagy slice lehet.') def __add__(self, másik_sorozat): if not isinstance(másik_sorozat, type(self)): raise TypeError(f'Csak {type(self).__name__} típussal lehetséges az összefűzés.') # Python 3.10 előtt: # dd = {**self.sorozat, **{k+len(self):v for k, v in másik_sorozat.sorozat.items()}} dd = self.indexek_elemek | {k + len(self): v for k, v in másik_sorozat.indexek_elemek.items()} return type(self)(dd.values()) def forgat(sorozat, n: int): return sorozat[n:] + sorozat[:n] |
Az összefűzés műveletének megvalósításához az __add__ metódust kell definiálni az osztályban. Ennek argumentuma a célkitűzésünknek megfelelően egy másik Sorozat típusú objektum lehet. Ezért a metódustörzset egy erre vonatkozó típusellenőrzéssel kezdjük, és kivételt dobunk, ha nem megfelelő az argumentum típusa. Ezt követően az összefűzést érdemben megvalósító kód lényege, hogy a két sorozatkonténer belső szótárát kell egyesíteni. De ezt úgy, kell megtenni, hogy az egyesített szótárban az argumentumban kapott sorozat szótárának indexeinek értéke a másik szótár legnagyobb indexét követő indexértékkel kezdődjön, és ehhez igazodjon a többi indexérték is. Ez könnyen biztosítható úgy, hogy az egyesítendő második szótárt egy szótárépítő kifejezéssel hozzuk létre az argumentum szótárából úgy, hogy annak indexértékeit megnöveljük az első szótár elemszámával. Az egyesített szótárt végül a Sorozat konstruktorkifejezésébe helyezzük és ezt adja vissza az __add__ metódus.
Mindezt áthatjuk a fenti kódban. A szótáregyesítést a Python 3.10 verziójától a | operátorral lehet egyszerűen megtenni. De kommentben feltüntettük azt is, hogy miként lehet ezt megvalósítani 3.10 előtt verzió esetén.
Az így továbbfejlesztett, egyéni, Sorozat típusú konténerek helyes működését az alább látható tesztek mutatják.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# TESZT sr1, sr2 = Sorozat('ABCDEFGH'), Sorozat(range(1, 9)) print(sr1, sr2) # Eredmény: 【A, B, C, D, E, F, G, H】 【1, 2, 3, 4, 5, 6, 7, 8】 print(sr1[2], sr2[-2]) # Eredmény: C 7 print(forgat(sr1, 2), forgat(sr2, -2)) # Eredmény: 【C, D, E, F, G, H, A, B】 【7, 8, 1, 2, 3, 4, 5, 6】 # Két Sorozat konténer összefűzésének eredménye. print(sr1 + sr2) # Eredmény: 【A, B, C, D, E, F, G, H, 1, 2, 3, 4, 5, 6, 7, 8】 # Sorozat példányok szeletei. print(sr1[2:4:1]) # Eredmény:【C, D】 print(sr1[::2]) # Eredmény:【A, C, E, G】 print(sr1[-1::-1]) # Eredmény:【H, G, F, E, D, C, B, A】 print(sr1[::-1]) # Eredmény:【H, G, F, E, D, C, B, A】 print(sr2[:-3]) # Eredmény:【1, 2, 3, 4, 5】 print(sr2[3:]) # Eredmény:【4, 5, 6, 7, 8】 print(sr2[:-3:-1]) # Eredmény:【8, 7】 print(sr2[6:1:-1]) # Eredmény:【7, 6, 5, 4, 3】 |
Itt azt is megfigyelhetjük, hogy azáltal, hogy az osztályunk már képes a szeletképzésre, az elemforgatást végző függvényben a szeletképzésre építő megvalósítást tudjuk alkalmazni. Arra is lenne lehetőség, hogy a saját, Sorozat típusú konténerünk elemeit más típusú sorozatkonténer (pl. list vagy tuple) elemeivel fűzzük össze, de ez már egy összetettebb feladat, amire nem térünk ki. A Python tudásépítés lépésről lépésre című e-könyvben viszont részletesen szó van különböző műveletek heterogén típusú argumentumokkal való megvalósításáról.