Számos esetben szükség lehet táblázatos formába rendezett számokkal – matematikai néven mátrixokkal – való munkára. Ha csak a legalapvetőbb műveleteket kell végeznünk, és a mátrixunk mérete nem túl nagy (pár száz sor és oszlop), akkor nem szükséges erre specializált külső könyvtárakat, csomagokat igénybe venni (ezzel is csökkentve a függőségeket), mert a Python alapból kínált nyelvi eszközeivel is létrehozhatunk mátrixot és egyéb olyan függvényeket, amelyekkel az igények kielégíthetők. /Nem beszélve arról, hogy milyen sokat lehet minden ilyen esetben problémamegoldásban és nyelvhasználatban fejlődni. De nyilván, ha gyorsan kell eredményt produkálni és járatosak vagyunk egy megfelelő külső csomagban, akkor azt kell használni./
A következőkben a szükséges függvények definícióját mutatjuk be, amelyek megalkotásánál több szempontot is figyelembe vettünk:
- ahol lehet, igyekeztünk a közbenső műveleteknél minél kevesebb értéket tárolni, mert minél nagyobb a mátrix annál több memóriát igényelhet a feldolgozási folyamat. A közbenső tárolás helyett törekedtünk egy adott érték azonnali feldolgozására. Nyelvi szinten ez azt jelenti, hogy konténerek helyett, ahol lehetett iterátorokat alkalmaztunk. Természetesen a végeredményre ez nem igaz, mert az többnyire lista vagy tuple lesz.
- minél egyszerűbb, átlátható kódra törekedtünk. Ehhez például az egymásba ágyazott for-ciklusok helyett előnyben részesítettük és intenzíven alkalmaztuk a listaépítő kifejezést (list comprehension), a generátorkifejezést, a map() és zip() függvényt, valamint az elemkicsomagolás műveletét. Ezzel a függvények kódja rövid és könnyen értelmezhető.
- a mátrix sorértékekből történő létrehozásakor olyan megoldást választottunk, amelynél az egyes mátrixsorok értékeit nem csak konténerekkel (list vagy tuple), hanem bármilyen véges számú értéket szolgáltató iterálható objektummal megadhatjuk.
Az alábbiakban láthatók a függvénydefiníciók, és utána némi magyarázat 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 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 |
import operator from typing import Iterable def mátrix_azonos_elemekkel(sorok_száma: int, oszlopok_száma: int, elem=0): return [[elem] * oszlopok_száma for _ in range(sorok_száma)] def mátrix_sorokból(*sorok: Iterable): mátrix = [[*sor] for sor in sorok] if len({len(s) for s in mátrix}) > 1: raise ValueError('Nem azonos sorhosszak!') return mátrix def transponált(mátrix): return [*zip(*mátrix)] def sor(mátrix, index): return tuple(mátrix[index]) def oszlop(mátrix, index): for i, t in enumerate(zip(*mátrix)): if i == index: return t def mátrix_összeadás(mátrix1, mátrix2): return [[*map(operator.add, sor1, sor2)] for sor1, sor2 in zip(mátrix1, mátrix2)] def skalárszorzat(vektor1, vektor2): return sum((map(operator.mul, vektor1, vektor2))) # A sum((x * y for x, y in zip(vektor1, vektor2))) is hasonlóan jó lenne. def mátrix_szorzás(mátrix1, mátrix2): return [[sum((map(operator.mul, sm1, om2))) for om2 in zip(*mátrix2)] for sm1 in mátrix1] def mátrix_string(mátrix: list[list]): sorok_száma, oszlopok_száma = len(mátrix), len(mátrix[0]) # 1. meghatározzuk az egyes oszlopok karakterekben mért maximális elemhosszait. max_számhosszok_oszlopokban = [max([len(str(e)) for e in o]) for o in zip(*mátrix)] # 2. Előállítjuk a mátrixot karakterlánc formában. # Az egyes sorok elemeit az adott oszlop max mezőszélességével formázzuk. mátrix_string = '' for si in range(sorok_száma): for oi in range(oszlopok_száma): mátrix_string += '|{:>{szélesség}}'.format(mátrix[si][oi], szélesség=max_számhosszok_oszlopokban[oi] + 2) mátrix_string += '|\n' return mátrix_string # TESZT mx1 = mátrix_sorokból((1, 2, 3), (e for e in range(4, 7)), (7, 8, 9)) mx2 = mátrix_azonos_elemekkel(3, 3, 1) print('Mátrix1:\n{}'.format(mátrix_string(mx1))) print('Mátrix2:\n{}'.format(mátrix_string(mx2))) összeg_mátrix = mátrix_összeadás(mx1, mx2) print('Összeg mátrix:\n{}'.format(mátrix_string(összeg_mátrix))) szorzat_mátrix = mátrix_szorzás(mx1, mx2) print('Szorzat mátrix:\n{}'.format(mátrix_string(szorzat_mátrix))) # Eredmények: Mátrix1: | 1| 2| 3| | 4| 5| 6| | 7| 8| 9| Mátrix2: | 1| 1| 1| | 1| 1| 1| | 1| 1| 1| Összeg mátrix: | 2| 3| 4| | 5| 6| 7| | 8| 9| 10| Szorzat mátrix: | 6| 6| 6| | 15| 15| 15| | 24| 24| 24| |
Az első két függvény a mátrix létrehozására használható. Mindkét esetben a mátrixot egy lista jelenti, amelynek sorértékeit a sorok számának megfelelő számú lista tárolja. Elvben lehetne tuple-t is használni, de mivel a mátrix elemeit később lehet, hogy módosítani akarjuk, így a változtatható list konténer szükséges.
Az első függvénynél a mátrixot a méretei (sorok és oszlopok száma) alapján hozzuk létre úgy, hogy a mátrix minden eleme egyetlen, megadható érték lesz (alapban 0). Ez jól használható ritka mátrixok előállítására, ahol a 0 értéken kívül kevés más érték szerepel (pl. egységmátrix, ahol a főátlóban 1-ek vannak).
Látható, hogy a sorelemek meghatározásakor elemtöbbszörözést alkalmaztunk. Felmerülhet a kérdés, hogy miért nem tettük ugyanezt a sorokkal is, hiszen azonos elemű mindegyik. Vagyis miért nem ez szerepel: [[elem]*oszlopok_száma]*sorok_száma? Azért, mert ezzel egy olyan mátrix jönne létre, amelyben minden sort azonos lista objektum képvisel. Ez azt jelenti, hogy ha egyik sorban egy elemet módosítunk, akkor az a többi sorban is változna.
A második függvényt az elsővel ellentétben éppen akkor érdemes használni, ha az elemek különbözőek. Ennek argumentumként a mátrix egyes sorainak értékét szolgáltató iterálható objektumokat kell felsorolni, nyilván annyit, amennyi sor kell, hogy legyen a mátrixban. Azt ellenőrzi a függvény, hogy az egyes sorok azonos elemszámmal rendelkeznek-e, ha nem, akkor hibaüzenetet küld, hiszen nem lenne egyértelmű az oszlopok száma.
Az egyik alap mátrixművelet a mátrix sorainak és oszlopainak a felcserélésével új mátrix előállítása. A művelet neve transzponálás, a létrejövő új mátrix a transzponált mátrix.
A harmadik függvény ezt valósítja meg elég egyszerű módon, mégpedig a zip() függvény tulajdonságát kihasználva. Amint látható, a zip() argumentuma a mátrix elemei kicsomagolva, vagyis a mátrix sorait tartalmazó listák. A zip() kérésre a neki megadott sorozatok azonos indexű elemeit veszi sorban egymás után, és ezekből képez újabb és újabb tuple objektumokat. A mátrix sorainak azonos indexű elemei épp egy mátrixoszlop értékeit jelentik, ezért a zip() végeredményben a mátrix oszlopait fogja egymás után kiadni. Ha a zip() így előállított elemeit egy listába csomagoljuk ki, akkor e lista egy mátrix lesz, aminek a sorait a kiinduló mátrix oszlopai alkotják. Vagyis az argumentumban átadott mátrix transzponáltját adja vissza a függvény.
Ha egy adott indexhez tartozó sor vagy oszlop elemeit akarjuk megkapni, akkor a következő két függvény szolgál erre. A sor kikérése nagyon egyszerű, mivel a mátrix elemei a sorok, tehát a mátrixlista indexelésével célt érünk. A függvény visszatérési értékénél látható tuple konverzió nem feltétlenül szükséges, de mivel a sor kikérésre többnyire az elemértékek kiolvasása céljából történik, ezért ilyen esetben általában tuple a szokásos sorozat típus.
Nem ennyire egyszerű egy adott oszlop kinyerése. De a fentebb említett transzponálással célt érünk. De nem úgy, hogy előállítjuk a transzponált mátrixot, mert az nagy mátrixnál sok memóriát foglalna le. Hanem csak generáltatjuk az oszlopokat a zip() függvénnyel a korábban látott módon, és számláljuk minden egyes kiadott oszlopot. A számlálást az enumerate() függvénnyel egyszerűen megtehetjük. Ha a számlálási érték megegyezik az argumentumban megadott index értékkel, akkor leállunk az iterációval és az aktuális oszloppal a függvény visszatér.
A mátrixok összeadását végző függvényt láthatjuk következőként. Ugyebár ilyenkor egy olyan új mátrixot kell előállítani, amelynek adott sor és oszlop indexhez tartozó eleme a két összeadandó mátrix ugyanezen pozícióban levő elemeinek összege. A megvalósításban a zip() függvénnyel az azonos indexű sorokat kérjük ki egy listaépítő kifejezésben. Ennek elemalkotó kifejezését egy lista képezi (ezek lesznek új mátrix sorai), amelyben az összeadandó mátrixok egyes sorait egy map() függvény dolgozza fel úgy, hogy az elemeiket összeadja. Az összeadást az operator modul add függvényével végezzük.
A mátrixok szorzása szintén az alapműveletek közé tartozik. A szorzatmátrix úgy is képezhető, hogy annak elemeit a bal oldali mátrix sorainak és a jobb oldali mátrix oszlopainak mint vektoroknak a skaláris szorzatai adják. Ezért először végig gondoljuk, hogy hogyan számoljuk ki vektorok skalárszorzatát. A skaláris szorzás abból áll, hogy a két vektor azonos indexű elemeit összeszorozzuk, majd a kapott szorzatokat összegezzük. Az ezt megvalósító függvényben két megoldást is feltüntettünk. Mivel a map() kombinálva az operátor modul mul() függvényével egyszerűbbnek tűnik, így végül ez adja a visszatérési értéket.
Miután tudjuk, hogy hogyan végezzünk skaláris szorzást, a mátrixszorzás implementációjában ezt fel tudjuk használni. E függvénynek a logikája hasonlít a mátrixösszeadásnál látotthoz, vagyis a visszatérési értéke egy, a szorzatmátrixot megvalósító lista, amelyet listaépítővel állítunk elő. Ebben vesszük a bal oldali mátrix sorait, majd a jobb oldali mátrix oszlopait (amelyeket a transzponált mátrix sorai képeznek) és ezek mint vektorok skaláris szorzataiból építünk listákat, melyek a szorzat mátrix sorai lesznek.
Végül, ahhoz, hogy a mátrixot táblázatszerűen lássuk, még egy függvényt készítünk, amely egy olyan karakterláncot állít elő, amelyet kiprintelve ezt az elrendezést jeleníti meg. A függvénytörzs működésének a megértését a kommentek segítik.
Ha a mátrixot osztályként akarnánk megvalósítani, akkor a bemutatott függvények könnyen adaptálhatók a metódusdefiníciókhoz.
Ha a függvényekben használt nyelvi elemek és szerkezetek működésében bizonytalanok lennénk, a Python tudásépítés lépésről lépésre című e-könyv megfelelő fejezeteiben ezekről részletes magyarázat található további példákkal.