Hogyan állapítsuk meg, hogy két szöveg anagramma-e?

Írjunk egy függvényt, amely az argumentumként kapott két karakterláncról megállapítja, hogy anagramma-e. Ha igen, akkor True, ha nem, akkor False értékkel tér vissza.

A feladat hasonló, mint az előző, palindromokkal foglalkozó bejegyzésben, és nem véletlenül azt követi. Ugyanis itt is attól függ a megírandó függvény, hogy milyen tágan definiáljuk az anagrammát. Most legyen ez a legtágabb, és ekkor azt mondhatjuk, hogy az anagramma egy szöveg hangjainak átcsoportosításával alkotott új értelmes szöveg. Tehát a betűk szóközökkel való tagolása, a kis- és nagybetűk különbözősége, valamint az írásjelek nem számítanak. Sőt azt is megengedjük, hogy a hosszú és rövid magánhangzók ugyanannak számítsanak.

Így definiálva az anagrammát a feladat elég könnyen megoldható, mert elsőként az előző, palindromellenőrzésről szóló bejegyzésben mutatott módon a két vizsgálandó szöveget kisbetűsre alakítjuk, majd kiszűrjük belőle a szóközöket és írásjeleket. Ezt követően pedig az így kapott két karakterláncot kell összehasonlítani abból a szempontból, hogy a) azonos karakterek vannak-e mindegyikben és b) ugyanazon karakterek előfordulási száma megegyezik.

Ezt a vizsgálatot legegyszerűbben a collections modul Counter objektumával tudjuk elvégezni úgy, hogy a két, szóközöktől, írásjelektől „megtisztított” kisbetűs szöveggel (s1, s2) egy-egy Counter példányt hozunk létre és ezek értékegyenlőségét ellenőrizzük. Ha egyenlőek, akkor anagrammáról van szó, és így a függvényünk True értéket kell, hogy visszaadjon.

E megfontolások alapján készített függvénydefiníciót láthatjuk alább legfelül.

Ez a függvény működik, de a függvénytörzs kialakítása azért nem túl szerencsés, mert láthatón több helyen a forráskód ismétlése történik: az első két sorban látványosan, de a return után is csak az argumentumokban van eltérés. A forráskódismétlés elkerülésére törekvés általában tanácsos, mert fokozott hibalehetőséget hordoz, hiszen módosítás esetén extra figyelem kell ahhoz, hogy minden ismételt előfordulásnál a változtatás megtörténjen.

Ezeket az ismétléseket küszöböljük ki a függvény egyes, rákövetkező változataiban.

Elsőként az s1 és s2 karaktersorozatokat állítjuk elő más módon, hogy csak egyszer kelljen a lower(), valamint a translate() és maketrans() metódusokat meghívni.

Ezt három lépéssel érjük el:

  1. A szöveg1 és szöveg2 kisbetűssé alakításával. Ezt egy generátorkifejezésben tesszük meg, amelyben a szöveg1 és szöveg2 változókat egy tuple konténerbe tesszük, majd sorban kikérve ezen elemeket meghívjuk rájuk str.lower() függvényt. Ezt azért tudjuk így megtenni, mert a lower() metódus paraméter nélküli, és így az osztályobjektumból hivatkozva függvényként tudjuk a generátorépítőben elemalkotó kifejezésként használni.
  2. Az így kapott kisbetűs szövegeket egy újabb generátorépítő kifejezésben használjuk és meghívjuk mindegyikre a translate() metódust, aminek az argumentuma a str.maketrans() eredménye.
  3. A generátorkifejezés elemeit (amelyek immár szóközöktől és írásjelektől mentes kisbetűs szövegek) kicsomagoljuk az s1 és s2 változóba.

/Megjegyezzük, hogy a fenti lépéseknél alkalmazhattunk volna listaépítőt is (list comprehension), de mivel nem akarjuk tárolni az elemeket, hanem csak egyszer kiolvasni, ezért itt célszerűbb a generátorkifejezés alkalmazása./

Az így módosított függvényt láthatjuk másodikként.

A return utáni kifejezést is meg tudjuk változtatni úgy, hogy a Counter csak egyszer szerepeljen. Az előbbihez hasonló elv alapján, azaz felhasználva az s1 és s2 karaktersorozatokat, egy generátorkifejezésben képezzük a Counter példányokat. Ahhoz, hogy utána ezeket sorban kikérve az egyenlőségvizsgálat megtörténhessen nem tudjuk használni az == operátor. Ezt helyettesíteni kell egy ekvivalens függvénnyel, amely két argumentum egyenlőségét teszteli. Az operator modul eq() függvénye éppen erre szolgál. Alkalmazzuk tehát ezt, és ennek argumentumaként adjuk át a Counter példányok generátorát mégpedig a * operátorral kicsomagolva, hogy biztosítsuk az eq() két bemenő értékét.

Ezt a változatot mutatja a harmadik függvénydefiníció, amivel el is értük a célunkat, hogy ne legyen kódismétlés.

Ha jól megnézzük e függvény törzsét, akkor könnyen észrevehető, hogy a return utáni generátorkifejezésben az s1 és s2 változókból képeztünk egy tuple típusú kételemű sorozatot. A felette levő sorban viszont éppen ellenkezőleg, a sorozatot kicsomagoltuk e két változóba. Ebből következik, hogy a második sorban szereplő generátorkifejezést be tudjuk helyettesíteni a return utáni (s1, s2) tuple helyére. És, ha még a legelső sort sem írjuk külön, hanem behelyettesítjük a második sor generátorkifejezésében szereplő kisbetűsek változó helyére, akkor a negyedikként felírt függvénydefiníciót kapjuk, amelyben a feladatot egyetlen kifejezéssel oldottuk meg.

Ez tetszetős, de mégsem ajánlott. Két okból sem. Az egyik, hogy olyan összetett lett a kifejezés, hogy az már az olvashatóság, azaz a könnyű áttekinthetőség és érthetőség rovására megy. A másik ok, hogy egy elég összetett logikát sikerült egy tömör kifejezésbe gyúrni, ami viszont a tesztelhetőséget nehezíti meg nagy mértékben. Ha elrontottunk volna valamit, akkor anélkül, hogy szét ne szednénk a kifejezést, nehéz lenne megállapítani, hogy hol, melyik részben van a hiba.

Ugyanez a gond, ha egymásba ágyazott map() függvényekkel oldanánk meg a feladatot, ahogy azt az ötödik függvényben látható. Ráadásul itt a sorozatelemekre a paraméteres translate() metódust nem is tudjuk meghívni. Ezen persze lehet segíteni, ha alkalmazzuk az operator modul methodcaller objektumát, de ez szintén nem kedvez a könnyű olvashatóságnak. A feladatban használt nyelvi elemek és szerkezetek (pl. generátorkifejezés, iterálható objektumok kicsomagolása, Counter, eq(), methodcaller és az operator modul egyéb függvényei) részletes jellemzői és használatuk példákkal együtt megtalálható a Python tudásépítés lépésről lépésre című e-könyvben.

É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.