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.
|
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# Python 3.12+ from itertools import product, chain, count, dropwhile, starmap from collections import deque from timeit import timeit from typing import Sequence type Matrix = list[list[int | float | complex]] def find_indices_nested_forloop(value, matrix: Matrix): for ri in range(len(matrix)): for ci in range(len(matrix[0])): if matrix[ri][ci] == value: return ri, ci def find_indices_forloop_product(value, matrix: Matrix): for ri, ci in product(range(len(matrix)), range(len(matrix[0]))): if matrix[ri][ci] == value: return ri, ci def find_indices_forloop_row_index(value, matrix: Matrix): for ri in range(len(matrix)): try: ci = matrix[ri].index(value) return ri, ci except ValueError: pass def find_indices_listcomprehension(value, matrix: Matrix): return [(ri, ci) for ri, ci in product(range(len(matrix)), range(len(matrix[0]))) if matrix[ri][ci] == value][0] def find_indices_genexp(value, matrix: Matrix): return next((ri, ci) for ri, ci in product(range(len(matrix)), range(len(matrix[0]))) if matrix[ri][ci] == value) def find_indices_chain_product_zip_filter(value, matrix: Matrix): item = next(filter(lambda z: z[1] == value, zip(product(range(len(matrix)), range(len(matrix[0]))), chain(*matrix)))) return item[0] def find_indices_chain_enum_filter(value, matrix: Matrix): colum_count = len(matrix[0]) item = next(filter(lambda z: z[1] == value, enumerate(chain(*matrix)))) return divmod(item[0], colum_count) def find_indices_chain_count_zip_filter(value, matrix: Matrix): colum_count = len(matrix[0]) item = next(filter(lambda z: z[1] == value, zip(count(), chain(*matrix)))) return divmod(item[0], colum_count) def find_indices_chain_enum_dropwhile(value, matrix: Matrix): colum_count = len(matrix[0]) item = next(dropwhile(lambda z: z[1] < value, enumerate(chain(*matrix)))) return divmod(item[0], colum_count) def find_indices_chain_tuple_index(value, matrix: Matrix): colum_count = len(matrix[0]) index = tuple(chain(*matrix)).index(value) return divmod(index, colum_count) def find_indices_chain_list_index(value, matrix: Matrix): colum_count = len(matrix[0]) index = list(chain(*matrix)).index(value) return divmod(index, colum_count) def find_indices_chain_deque_index(value, matrix: Matrix): colum_count = len(matrix[0]) index = deque(chain(*matrix)).index(value) return divmod(index, colum_count) def find_indices_sequence(value, vectorized_matrix: Sequence, column_count: int): index = vectorized_matrix.index(value) return divmod(index, column_count) |
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.
|
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 |
# TESZT cnt = count(1) row_count, column_count = 100, 100 # A teszthez használt mátrix sor- és oszlopszáma. mx = [[next(cnt) * 10 for ci in range(column_count)] for ri in range(row_count)] print('sorok száma =', row_count, 'oszlopok száma =', column_count) def test_time_performance(value, matrix: Matrix): """A megadott matrix adott value értékének sor- és oszlopindexeit meghatározó függvények végrehajtási idejét méri. Eredményképpen kiírja a végrehajtási idő szerint növekvő sorba rendezett függvények neveit az abszolút és relatív végrehajtási időkkel. """ # A find_indices_sequence() függvény méréséhez a vektorizált mátrix létrehozása különböző sorozattípusú konténerekkel. vectorized_matrix_tuple = tuple(chain(*mx)) vectorized_matrix_list = list(chain(*mx)) vectorized_matrix_deque = deque(chain(*mx)) col_count = len(matrix[0]) # Összegyűjtjük azon mérendő függvényeket, amelyek argumentuma az egymásba ágyazott listákkal megadott mátrix. function_names_and_objects = {obj_name: obj for obj_name, obj in globals().items() if obj_name.startswith('find_indices') and obj_name != 'find_indices_sequence'} # Megmérjük és nyilvántartjuk a függvények végrehajtási idejét. functions_searchtimes1 = [(func_name, 1000 * timeit(stmt=lambda: func_obj(value, matrix), number=100)) for func_name, func_obj in function_names_and_objects.items()] # Megmérjük a különféle sorozattípusú konténerekbe vektorizált mátrixokkal meghívott find_indices_sequence() függvény # végrehajási idejét. A nyilvántartásban a függvénynévben a sequence szó helyett az aktuális konténer típusát adjuk meg. functions_searchtimes2 = [('find_indices_tuple', 1000 * timeit(stmt=lambda: find_indices_sequence(value, vectorized_matrix_tuple, col_count), number=100)), ('find_indices_list', 1000 * timeit(stmt=lambda: find_indices_sequence(value, vectorized_matrix_list, col_count), number=100)), ('find_indices_deque', 1000 * timeit(stmt=lambda: find_indices_sequence(value, vectorized_matrix_deque, col_count), number=100)) ] # Az összes függvényt a mért végrehajtási idő szerint növekvő sorrendbe rendezzük. functions_searchtimes = sorted(functions_searchtimes1 + functions_searchtimes2, key=lambda t: t[1]) # A sorbarendezett függvénynevekhez a mért végrehajtási időt, valamint ennek a legkisebb időhöz viszonyított # relatív értékét társítjuk. functions_rel_searchtimes = ((func_name, search_time, search_time / functions_searchtimes[0][1]) for func_name, search_time in functions_searchtimes) # Kiírjuk a sorbarendezett függvények neveit az abszolút és relatív végrehajtási időkkel. print(*starmap('{:40} {:10.3f} {:10.3f}'.format, functions_rel_searchtimes), sep='\n') # A tesztfüggvényt arra a három esetre hívjuk meg, amikor a mátrix első, középső és utolsó elemének sor- és oszlopindexeit keressük. print('\nElső elem') test_time_performance(mx[0][0], mx) print('\nKözépső elem') test_time_performance(mx[row_count // 2][column_count // 2], mx) print('\nUtolsó elem') test_time_performance(mx[row_count - 1][column_count - 1], mx) |
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:
|
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# Python 3.12+ from __future__ import annotations import operator from itertools import chain, batched, repeat, product from math import sumprod from typing import Iterable class Matrix: def __init__(self, rowcount: int, columncount: int, elements: Iterable = ()): self.rowcount, self.columncount = rowcount, columncount if not elements: self._mx_seq = [0 for _ in range(self.rowcount * self.columncount)] else: seq = list(elements) if len(seq) != self.rowcount * self.columncount: raise ValueError('A megadott elemsorozat száma nem egyezik a sorok és oszlopok szorzatával.') self._mx_seq = seq @classmethod def from_nested_lists(cls, lists_of_list: list): rowcount, columncount = len(lists_of_list), len(lists_of_list[0]) return cls(rowcount, columncount, chain(*lists_of_list)) def __str__(self) -> str: max_num_width = len(str(max(self._mx_seq))) return '\n'.join((' '.join((f'{x:{max_num_width}}' for x in row)) for row in batched(self._mx_seq, self.columncount))) + '\n' def _rici_to_index(self, row_index: int, column_index: int) -> int: """A megadott sor- és oszlopindexből a reprezentációs lista megfelelő indexét adja vissza.""" return row_index * self.columncount + column_index def _index_to_rici(self, index: int) -> tuple[int, int]: """A megadott reprezentációs listaindexnem megfelelő sor- és oszlopindexszel tér vissza.""" return divmod(index, self.columncount) def as_tuple(self) -> tuple: """A mátrix reprezentációs lista elemeit egy nem változtatható tuple konténerben adja vissza.""" return tuple(self._mx_seq) def convert_to_nested_list(self) -> list: """A mátrixot listába ágyazott listák formájában reprezentálja és ezt adja vissza.""" return [list(row) for row in batched(self._mx_seq, self.columncount)] def __contains__(self, item) -> bool: return item in self._mx_seq def __eq__(self, matrix: Matrix) -> bool: return self.as_tuple() == matrix.as_tuple() def __hash__(self): return hash(self._mx_seq) def __getitem__(self, rowindex): return tuple(self._mx_seq[rowindex * self.columncount:rowindex * self.columncount + self.columncount]) def get_element(self, rowindex, columnindex): """A megadott sor- és oszlopindexszel azonosított elem értékével tér vissza.""" return self._mx_seq[self._rici_to_index(rowindex, columnindex)] def set_element(self, rowindex, columnindex, value): """A megadott sor- és oszlopindexszel azonosított elem értéke a value argumentum lesz.""" self._mx_seq[self._rici_to_index(rowindex, columnindex)] = value def clear_element(self, rowindex, columnindex): """A megadott sor- és oszlopindexszel azonosított elem értéke 0 lesz.""" self._mx_seq[self._rici_to_index(rowindex, columnindex)] = 0 def clear_all_elements(self): """Minden elem értéke 0 lesz.""" self._mx_seq = [0] * (self.rowcount * self.columncount) def get_row_and_column_indices(self, value) -> tuple[int, int] | None: """A megadott érték első előfordulásának sor- és oszlopindexével tér vissza. Ha az érték nem eleme a mátrixnak, a vissztérési érték None. """ if value not in self: return None return divmod(self._mx_seq.index(value), self.columncount) def get_all_row_and_column_indices(self, value) -> list: """Egy listával tér vissza, amely a megadott érték összes előfordulásának sor- és oszlopindexét tartalmazza, vagy egy üres listával, ha az érték nem eleme a mátrixnak. """ i, indices = 0, [] try: while i := self._mx_seq.index(value, i): indices.append(divmod(i, self.columncount)) i += 1 except ValueError: pass return indices def set_row(self, rowindex: int, *elements) -> None: """A sorindexszel azonosított sor elemei az elements argumentummal felsorolt értékek lesznek.""" if rowindex not in range(self.rowcount): raise ValueError('A sorindex nem megfelelő') if len(elements) != self.columncount: raise ValueError('Az elemek száma nem egyenlő az oszlopszámmal') self._mx_seq[rowindex * self.columncount:rowindex * self.columncount + self.columncount + 1] = elements def get_row(self, rowindex: int) -> tuple: return tuple(self._mx_seq[rowindex * self.columncount:rowindex * self.columncount + self.columncount]) def get_column(self, columnindex: int) -> tuple: return tuple(self._mx_seq[columnindex::self.columncount]) def transpose(self) -> Matrix: return Matrix.from_nested_lists([self.get_column(ci) for ci in range(self.columncount)]) def __add__(self, matrix: Matrix) -> Matrix: """Két mátrix összegmátrixával tér vissza.""" if (self.rowcount, self.columncount) != (matrix.rowcount, matrix.columncount): raise ValueError('A két mártix összeadása nem lehetséges, mert dimenzióik nem egyeznek meg.') return Matrix(self.rowcount, self.columncount, map(operator.add, self._mx_seq, matrix.as_tuple())) def __sub__(self, matrix: Matrix) -> Matrix: """Két mátrix különbségmátrixával tér vissza.""" if (self.rowcount, self.columncount) != (matrix.rowcount, matrix.columncount): raise ValueError('A két mártix kivonása nem lehetséges, mert dimenzióik nem egyeznek meg.') return Matrix(self.rowcount, self.columncount, map(operator.sub, self._mx_seq, matrix.as_tuple())) def __mul__(self, scalar: int | float | complex) -> Matrix: """Mátrix konstanssal való szorzás eredménymátrixával tér vissza.""" return Matrix(self.rowcount, self.columncount, map(operator.mul, self._mx_seq, repeat(scalar))) def __rmul__(self, scalar: int | float | complex) -> Matrix: return self * scalar def __matmul__(self, matrix: Matrix) -> Matrix: if self.columncount != matrix.rowcount: raise ValueError('A két mártix szorzása nem lehetséges, mert dimenzióik nem megfelelők.') elements = (sumprod(self.get_row(i), matrix.get_column(j)) for i, j in product(range(self.rowcount), range(matrix.columncount))) return Matrix(self.rowcount, matrix.columncount, elements) # TESZT if __name__ == '__main__': matrix1 = Matrix(5, 4, range(5 * 4)) matrix2 = Matrix(4, 5, reversed(range(4 * 5))) print('1. mátrix:', matrix1, sep='\n') print('2. mátrix:', matrix2, sep='\n') square_mx = 2 * ((product_mx := matrix1 @ matrix2) + product_mx.transpose()) print('A művelet eredményeként kapott négyzetes mátrix:', square_mx, sep='\n') print('A {} érték sor- és oszlopindexe: {}'.format(value := 2000, square_mx.get_row_and_column_indices(value))) print('A {} érték sor- és oszlopindexei: {}'.format(value := 1700, square_mx.get_all_row_and_column_indices(value))) # Eredmény: # 1. mátrix: # 0 1 2 3 # 4 5 6 7 # 8 9 10 11 # 12 13 14 15 # 16 17 18 19 # # 2. mátrix: # 19 18 17 16 15 # 14 13 12 11 10 # 9 8 7 6 5 # 4 3 2 1 0 # # A művelet eredményeként kapott négyzetes mátrix: # 176 532 888 1244 1600 # 532 824 1116 1408 1700 # 888 1116 1344 1572 1800 # 1244 1408 1572 1736 1900 # 1600 1700 1800 1900 2000 # # A 2000 érték sor- és oszlopindexe: (4, 4) # A 1700 érték sor- és oszlopindexei: [(1, 4), (4, 1)] |
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.