Hogyan határozzuk meg egy adott mátrixelem sor- és oszlopindexét?

Tegyük fel, hogy olyan adatokkal kell dolgozni, amelyek egy mátrixban reprezentálhatók, és a feldolgozás során többször is szükség van egy adott adat, azaz egy mátrixelem sor- és oszlopindexének meghatározására. Például táblázatos játéktérrel rendelkező játékok esetén egy bábu mozgatásához a programnak tudnia kell a bábu aktuális sor- és oszlopindexét (honnan mozdítjuk) és a cél sor- és oszlopindexét (hová mozdítjuk). De ilyen feladat merülhet fel táblázatos nyilvántartások (pl. vállalati készletnyilvántartás) esetén is például ahhoz, hogy meghatározható legyen egy adott termék mennyiségének vagy árának beazonosíthatósága az aktualizáláshoz.

Írjunk tehát olyan függvényt, amely argumentumai a kérdéses elem értéke és az adott mátrix, visszatérési értéke pedig egy kételemű tuple. Ez a kérdéses elem első előfordulásának sor- és oszlopindexét tartalmazza. A mátrixot egymásba ágyazott listákkal valósítjuk meg.

Ilyen függvényre számos megoldást ki lehet találni, ahogy azt alább láthatjuk. A függvénynevekben utalás történik az adott függvényben alkalmazott főbb nyelvi eszközökre.

Az első három függvény for-ciklust alkalmaz. Ezekből az első egymásba ágyazott ciklussal operál. Ezt olykor kevésbé olvashatónak tartják és helyette a szabványos könyvtár itertools moduljának product() iterátorát ajánlják. Ezt használja a második függvény. A harmadik függvényben csak a sorindexek iterálására használunk for-ciklust; az oszlopindexet a sorindex alapján azonosított sor mint listának az index() metódusával kapjuk meg.

A negyedik függvény listépítő kifejezést alkalmaz, amely sikeres találat esetén egyelemű listát eredményez, és ezzel az elemmel tér vissza. Az ötödik függvény hasonló elven alapul, de lista helyett egy generátorkifejezést alkalmazunk.

Az eddigi függvényekben a mátrix egymásba ágyazott listaként megvalósított szerkezetét használtuk ki. Egy mátrix azonban más módon is reprezentálható, mégpedig egy sorozattal. Ezt a mátrix vektorizálásának vagy vektorizációjának nevezik. Ennek elvét és az indexek közötti összefüggéseket mutatja a következő ábra.

Az egymásba ágyazott listaként megvalósított mátrix sorfolytonos vektorizálása könnyen elvégezhető az itertools modul chain() iterátorát használva. Nem kell mást tenni, mint a chain() argumentumában a mátrixot ki kell csomagolni. Ekkor a chain(*matrix) iterátor sorfolytonosan fogja szolgáltatni a mátrix elemeit.

Az előbbieket következő hét függvény a bemeneti mátrix vektorizált formáját alkalmazza a különböző nyelvi eszközökkel megírt logika megvalósításához. Ezekből az első öt memóriatakarékos módon határozza a meg a keresett indexeket, az utolsó három viszont a vektorizált mátrix elemeit különböző sorozattípusú konténerben (tuple, list és deque) tárolja és ezekre lesz az index() metódus meghívva.

A fenti kódsorban a legutolsó függvény ez utóbbiakat általánosítja úgy, hogy argumentumként egy olyan sorozattípusú konténert fogad, amely a mátrix vektorizálásának eredménye. Ebben a változatban a működéshez bemenetként a mátrix oszlopszámát is meg kell adni, mert a megadott sorozatból, annak hosszából csak a sor- és oszlopszám szorzatát lehetne megtudni.

Nos, a kitűzött feladat megoldásához a választék bőséges. A kérdés most már csak az, hogy a felsorolt függvények közül melyiket válasszuk.

Ha az adatfeldolgozás során sokszor kell egy elem helyét a mátrixban meghatározni, akkor nem mindegy, hogy az egyes függvényváltozatok milyen végrehajtási idővel rendelkeznek. Ezért a választáshoz megvizsgáljuk, hogy ugyanazon mátrix és adott keresett elem esetén az egyes függvényeknek milyen a végrehajtási ideje.

A futási időket a timeit modul timeit függvényével mérjük az alábbi tesztsorokkal. Itt három növekvő méretű mátrixra végezzük a mérést úgy, hogy minden egyes mátrix esetén keressük  a mátrix első, középső és utolsó elemének sor- és oszlopindexeit.

A tesztek eredményét a következő ábra táblázatai foglalják össze. Itt a függvények a futási idő szerint növekvő sorrendben szerepelnek. A rel fejlécű oszlopok a legkisebb végrehajtási időhöz viszonyított relatív értékeket mutatják, míg az abs fejlécű oszlopok a mért időket nem viszonyszámként, hanem abszolút értékként tüntetik fel.

Látható, hogy minden esetben a vektorizált mátrix sorozatát fogadó find_indices_sequence() nevű függvény volt a leggyorsabb, mégpedig jelentősen, lényegében függetlenül attól, hogy milyen típusú sorozatot adtunk bemenetként. /A táblázatban e függvény find_indices_tuple, find_indices_list és find_indices_deque néven szerepel utalva a bemeneti sorozat típusára/

E jó eredmény persze azt feltételezi, hogy a mátrix vektorizálása és a sorozattípusú konténer előállítása a find_indices_sequence() függvény hívása előtt már megtörtént és így ennek ideje az indexkeresésben már nem jelentkezik. Ha ez a feltétel nem áll fenn, akkor a find_indices_forloop_row_index() függvény a következő legjobb választás.

Ha nincs más ötletünk, mint az egymásba ágyazott for-cikluson alapuló megoldás /find_indices_nested_forloop()/, akkor ez sem tűnik rossz választásnak. Bár hasonló eredményt érhetünk el a függvényen belüli vektorizálással létrehozott sorozatkonténerekre meghívott index() metódust alkalmazó find_indices_chain_tuple_index(), find_indices_chain_list_index(), és find_indices_chain_deque_index() függvényekkel, de ezek – bár csak átmenetileg, a függvényhívás idejére – több memóriát igényelnek. A többi függvény ugyan memóriatakarékos, de az előbb említetteknél általában rosszabbul teljesít, ezért ezeket nem érdemes használni, ha sok indexkeresést kell végrehajtani.

Említettük, hogy a futási időben legjobban teljesítő indexkereséshez előfeltétel, hogy a mátrix vektorizálása és a sorozattípusú konténer előállítása az indexkeresés előtt már megtörténjen. Ezt tudjuk teljesíteni, ha egy olyan Matrix nevű osztályt definiálunk, amely az elemeket vektorizált formában tárolja. Ekkor a Matrix példányokban nem csak a tartalmazási vizsgálat és az elemek helyének meghatározása lesz gyors, hanem a mátrixműveletek is viszonylag egyszerűen, kevés kóddal megvalósíthatók. Egy lehetséges osztálydefiníciót mutatunk alább:

E bejegyzés a viszonylag szűk fókusza ellenére meglehetősen széles nyelvi ismereteket fed le. Ezek a Python tudásépítés lépésről lépésre című e-könyvben többek között a következő fejezetekben találhatók:

  • ciklusszervező szerkezetek és utasítások: „Repetázzunk! – ciklikus utasítás végrehajtás” fejezet,
  • a konténer- és iterátorépítés: „Különleges műveletek” fejezet,
  • kivételek és kivételkezelés: „Kivételes bánásmód – kivételek és kezelésük” fejezet,
  • függvények: „Egymáshoz rendelve – függvények”, a „Beépített függvények”, és „Különleges függvénydefiníciók” fejezetek,
  • osztályokr és egyéni osztályok definiálása: „Osztály vigyázz! – típuslétrehozás osztályokkal” és „Mágikus metódusok egyedi igényre szabott osztályokban” fejezetek,
  • az itertools modul és a végrehajtási idő mérése: „Készétel fogyasztás – a szabványos könyvtár moduljainak használata” fejezeten belül a „Speciális iterátorok” és „A programvégrehajtás felfüggesztése és a futási idő mérése” alfejezetek.

Érdekel a Python tudásépítés lépésről lépésre az alapoktól az első asztali alkalmazásig című e-könyv.