2013. december 26., csütörtök

Javascript futtatása Python kódból, PyV8 fordítása

Kellemes Ünnepeket mindenkinek!
Természetesen azért, mert megint nem írtam már hetek hónapok óta, még nem állt meg az élet. Mostanában web scraping-el foglalkozok, ami nagyon komplex egy feladat, persze csak ha az ember normálisan akarja megcsinálni.

Egy olyan problémába ütköztem, ami manapság egyre hétköznapibb, hogy a weboldal nagy részét Javascript kódból építik fel. Használhatnék amúgy erre egy valódi böngészőt is, ami futtatja a kapott kódot, és Python alól birizgálom, pl Selenium-al, de nekem csak a Javascript kódban tárolt adatokra van szükségem. Párperces keresgélés után nem találtam normális parsert a nyelvhez, így aztán úgy döntöttem, hogy felrakom a Google V8 engine-jét, a hozzá tartozó Python-os bindinggel, a PyV8-al, betápolom neki a kódot, és dolgozok a visszakapott értékekkel.

Szerencsére a kis Odroid-X2-n szépen -lassan- lefordul maga az engine, és a binding is. A dokumentációban nem nagyon van részletes leírás a fordítás menetéről, így ezt magamnak kellett kitalálnom. Szerencsére apt-ből mindent össze lehet szedni. Lássuk a parancsokat!
sudo apt-get install libboost-python-dev libboost-thread-dev libboost-system-dev
Ennyi az összes függőség a fordításhoz. Ha nincs svn kliens feltelepítve, akkor erre még szükség lehet:
sudo apt-get install subversion
Ezek után egyszerűen pip-el telepítjük
sudo pip install pyv8

Rövid példácska

Rendbe, ha már ilyen szépen feltelepült a lib, akkor azt is leírom hogy mire kellett.
Tehát, a feldolgozandó kódban, egyes HTML tag-ek onmouseover eventjén van az alábbihoz hasonló kód:
jQuery("#1337").html("Leet");
Azaz rámutatáskor ad 1337 id-ű tag tartalma legyen a Leet string. Ilyen egyszerű.

Nem akartam az egész jQuery-t betölteni a memóriába, ezért inkább írtam egy kis kamu függvényt, amivel a fenti sor futtatható. Így néz ki:
var jQuery = function (unused){
    html = function(data){
        return data
    }
    return this;
}
A jQuery függvény visszatér a this-el, ami úgy mellékesen a window. Maga a függvény nem csinál semmit se a paraméterével. A függvény törzsében definiálunk egy html nevű függvényt. Ez globális lesz, így a window.html-en keresztül is elérhető.
Tehát amikor a jQuery visszatér a this-el, ott lesz már a window.html is, így működni fog method chaining (igazából nem fog működni, ahhoz megint csak this-el kellett volna visszatérni, de nekem most így is megfelel a kód).

Rakjuk össze az egészet és próbáljuk ki:

import PyV8
ctx = PyV8.JSContext()
ctx.enter()
ctx.eval('var jQuery=function(unused){html=function(data){return data};return this;}')
ctx.eval("jQuery('#1337').html('Leet')")
#>>>'Leet'

Először létrehozunk egy új contextet és bele is lépünk. Ez után betöltjük a függvényt, aztán kipróbáljuk hogy visszakapjuk-e a html-nek adott stringet, természetesen a Python interpreterben.
Az utolsó sorban látszik hogy ez megtörtént, úgyhogy nagy az öröm!

-slp

2013. július 14., vasárnap

zvbi-0.2.34 fordítása Ubuntu 13.04 alatt

(Ez a bejegyzés -már megint- nem Pythonnal foglalkozik)
A zvbi egy olyan linuxos lib amiben mindenféle tool található amivel a VBI alatt kiküldött adatokat lehet dekódolni. Szerintem igazából minden csomagkezelőben megtalálható, viszont a forráskódjában van egy test mappa, és az abban található eszközöket nem rakja fel az apt-get. Az egyik ilyen program egy segédeszköz, ami megjeleníti a VBI device kimenetét képként, így nagyon sokat segít a debuggolásban. Mivel leírást nem találtam róla hogy hogyan kell lefordítani a zvbi-t forrásból ezért most leírom ide.

Patchelés

Okulva a rengeteg rossz tapasztalatból úgy álltam neki a fordításnak, hogy ez se fog elsőre menni. Igazam lett. A gcc meghal majd a következő errorral:
date.c:96:3: error: incompatible type for argument 2 of ‘clock_settime’

Szerencsére sikerült kézzel kijavítanom. Így utólag visszagondolva lehet hogy csak a kódot tettem fordíthatóvá de a program maga szar maradt, de ameddig én próbálgattam semmi se dobott segfaultot. Csináltam hozzá gyönyörűséges patchet, amit innen le lehet tölteni.

Patcheljük meg a kódot. Aki nem csinált még ilyet (pl én) annak:

Töltsük le a patchet a forrás gyökerébe, aztán alkalmazzuk
wget https://dl.dropboxusercontent.com/u/58194064/zvbi_date_clock_settime.patch
patch -p1 < zvbi-ftbfs.patch

Függőségek

Épp ebben a pillanatban raktam fel egy teljesen szűz Ubuntut, és ezen végzem a fordítást. A lib maga lefordul minden extra nélkül, de az osc-hez szükség van a libx11-dev-re
sudo apt-get install libx11-dev

Fordítás & telepítés

Innentől már nem is annyira izgalmas:
./configure
make
sudo make install

osc

A make install nem rakja jól látható helyre az osc programot (vagy csak én nem találom). A zvbi forrásának könyvtárában a test mappában van mindenféle csoda, ebből az osc kell. A README fájlban van részletes leírás az összes toolhoz. Az osc röviden:

./osc -d /dev/vbi0

Egy kép

Mindenki szereti a képeket

Igen, a terminálban 0.2.33 van. Amikor elkezdtem írni a bejegyzért akkor a 0.2.33-at szedtem le, de eszembe jutott hogy már van 0.2.34 is. Vicces mód mivel az se fordult le egyből, így nem kellett a 0.2.33-hoz íródott bejegyzésemet kidobnom, csak át kellett írnom a verziószámot a szövegemben és a hibaüzenetet. Azért ez igazából kicsit égő, mármint hogy az új verzió se fordul hiba nélkül.

2013. július 9., kedd

Hol az új post?

Meglehesőgen régóta nem volt már megint frissítés, pedig csak a meglevő szövegeimet kéne áthúznom ide. Ami azt illeti, most kivételesen jó alibim van, mert hát vizsgáztam, dolgozok, demopartyn jártam és közben masszívan 1 projektre fókuszálok aminek eléggé gyér a szakirodalma pláne magyarul. Ezért is fogok majd beszámolni róla pontosan, illetve forráskóddal is jövök.
Már nagyon szeretnék beszámolni róla, de a vicc hogy több hónapos munka van már benne, és még mindig nem működik.
A lényeg hogy élek, de egy nagyon fontos probléma megoldásán dolgozok, ezért nincs most frissítés.

2013. április 18., csütörtök

04 - Beolvasás, kiírás

Még mielőtt akárminek is nekikezdenék, szeretném jelezni, hogy igazodva a kor igényeihez, ez a bejegyzés 2016. március 5-én újraírásra került. Az eredeti bejegyzés még a Python 2.7.3 használatával készült, és átkozott hosszú volt. Azóta gyakorlatilag az összes jelentős csomag Python 3 kompatibilis verziója elkészült, és magam is napi rendszerességgel Python 3 nyelvű fejlesztéssel foglalkozok.

Mostanra már mindent tudunk a változókról, operátorokról, kifejezésekről, viszont úgy gondolom, hogy kezd elég unalmas lenni, hogy a programjaink csak úgy lefutnak oszt’ csá. Szóval ebben a részben megnézzük hogy hogyan lehet olyan programot írni, ami képes adatot fogadni a felhasználótól. A bejegyzés másik felében pedig kicsit részletesebben is átvesszük, hogy mire is képes a már korábban elég sűrűn használt print().

Na akkor toljuk, mert sok a teendő.

Adat beolvasása a felhasználótól

Ahhoz hogy a programunk megálljon egy adott ponton, és várjon arra hogy begépeljünk valamit, egy függvényt kell használnunk, ez az input().

Amikor a program futása az input() függvény hívására érkezik, a program egész addig fog állni és várni, amíg Enter billentyűt nem nyomunk. Ekkor megkapjuk a programunkban a begépelt szöveget, ami minden esetben kizárólag str típusú lesz. Igen, akkor is, ha számjegyeket írtunk be.

Oké, akkor lássuk hogy hogy is működik ez:

username = input()
print(username)

Elindítjuk a programot a konzolban, és a villogó kurzoron kívül nem látunk mást. Látszólag olyan mintha lefagyott volna a program, pedig valójából csak várja hogy begépeljünk valamit. Írjunk be bármilyen tetszőleges szöveg, és a végén nyomjunk Enter-t. A gépelésünk alatt azonnal megjelenik még egyszer a szöveg, és kilép a program.

Lássuk tételesen hogy mi történt:

  1. A program futása az input()-ra érkezett. Itt várakozott a program.
  2. Lenyomtuk az Enter-t, így véget ért a várakozás. Az input() vissza tért a begépelt szöveggel, amit az értékadó operátor eltárolt az username nevű változóban
  3. A 2. sorban a print() kiírta a username változó értékét, ami pont a begépelt szöveg.

Függvényhívás röviden

Bár ez a bejegyzés nem a függvényekről szól, de hát ugye már itt a print() és most az input() is. Tehát mi ez a függvényesdi?

A függvényeket arról lehet felismerni, hogy hasonlóan kell leírni a nevüket mint egy változóét, de a végükre zárójelet kell tenni. A print() esetén már láttuk korábban, hogy a zárójelek közé beírhatunk kifejezéseket is. Azzal hogy leírtuk a zárójelet, megtörténik a függvény hívás.

A függvények működéséről egyelőre elég annyit tudni, hogy valamilyen forráskódot rejtenek magukban. Ez a forráskód várhat különböző adatokat. Ezen adatokat soroljuk fel a zárójelek közé írva, és argumentumnak hívjuk őket. A függvény a futása mindig eredményez valamilyen adatot, amihez hozzáférünk majd ott, ahol meghívtuk a függvényt. Ezt az adatot hívják visszatérési értéknek.

Ezt mondhatjuk úgy is, hogy a függvényhívás mint kifejezés kiértékelésének eredménye a függvény visszatérési értéke.

Hogy hívnak?

Sokkal értelmesebb lenne a fenti program, ha először kérdezne, és csak aztán várakozna a program. Akkor írjunk egy olyan programot, ami megkérdezi a felhasználótól a nevét, és utána illedelmesen köszönti.

print('Hogy hívnak?')
name = input()
print('Szia ' + name)

A program először kiírja a Hogy hívnak? üzenetet, majd kurzor leugrik a következő sorra és várja hogy beírjunk valamit. Ez eltárolódik majd a name változóban. A print()-ben pedig jól kiírjuk a begépelt nevet, és még az elejére ragasztjuk azt hogy Szia.

Rövidebb forma

Az a művelet-kombináció elég gyakori, hogy ha be akarunk olvasni valamit a felhasználótól, akkor előtte kiírunk egy üzenetet. Pontosan ezért, az input() paraméterül kaphat egy kifejezést, praktikusan egy str-t, amit először kiír úgy mint a print(), és csak utána kezd várakozni a begépelésre. Nem mellékesen ilyenkor rögtön a kiírás mellett villog a kurzor, ami esetenként szebb, mint ha tök új és üres sorba gépelnénk.

Akkor írjuk át a fenti példát:

name = input('A neved: ')
print('Szia ' + name)

Figyeljetek rá, hogy a kiírt string végén legyen szóköz, máskülönben tök hülyén fog kinézni a konzolban, hogy nem látszódik hogy hol ért véget az a szöveg amit a program írt ki, és az amit a felhasználó kezdett beírni.

Számológép

Írjunk egy olyan egyszerű számológépet, ami beolvas két számot, összeadja őket és kiírja az eredményt. Mi sem egyszerűbb ennél:

a = input('Első szám: ')
b = input('Második szám: ')
c = a + b
print('Az összeg: ' + str(c))

Az első két sorban kiírogatjuk a formai dolgokat. Az első szám az a, a második szám pedig a b nevű változóban kerül eltárolásra. Összeadjuk ennek a két változónak az értékét és eltároljuk az eredményt a c változóban. A program végén pedig kiírjuk hogy Az összeg:. Fontos: mivel ugye c változó számot tartalmaz, ezért ezt először át kell alakítani str-é, hogy elé tudjuk rakni a kis feliratot. Ha ezt nem tesszük akkor hibát kapunk.

Akkor most beírok két számot, nálam ez történik:

Első szám: 5
Második szám: 6
Az összeg: 56

Az meg mi a lófasz hogy 56?

Típuskonverzió

Aki esetleg elfelejtette, az input() mindig string típusú értéket eredményez. Tehát hazudtam az előbb amikor azt írtam hogy “… mivel ugye a c változó számot tartalmaz …”. Az is, meg az a és a b is végig str-ek voltak.

Ha összeadunk két str-t akkor mit kapunk? A két stringet eggyé fűzve. Pontosan ezért tud megjelenni az 56 a képernyőn, ami a memóriában valójából '56', és nem 56 !

Ahhoz tehát hogy számként dolgozhassunk a kapott adattal azt számmá kell alakítani.

Számológép ami számol is

A fentiek szellemében javítsuk ki a programot. Feltételezzük hogy int (egész) értéket kapunk majd:

a = int(input('Első szám: '))
b = int(input('Második szám: '))
c = a + b
print('Az összeg: ' + str(c))

És tápoljunk be pár értéket:

Első szám: 5
Második szám: 6
Az összeg: 11

Na erről van szó kérem szépen.

Lehet elsőre émelyítő ez a sok zárójel az 1. és 2. sorban, de a Pythonnak ez meg se kottyan. Ahogy korábban írtam, a legtöbb kifejezés egymásba ágyazható. Amit a kódban írtam igen gyakori, tehát hogy a típuskonverziót végző függvény egyből megkapja argumentumul az input() visszatérési értékét.

Első szám: kettő

Nekem elhihetitek, mindig van valaki aki poén adatokat írogat be a programba. Ha most mi is így teszünk, és az első kérdésre nem számjegyeket, hanem valami szöveget, sőt, egy törtet írunk be, akkor a program kilép hibával, és valami ValueError-t lehel ki utolsó lélegzetéből.

Ezzel egyelőre nem foglalkozunk, de lehet lelkiekben készülni arra, hogy ahhoz hogy ne lehessen csak úgy összeomlasztani a programunkat, kezelnünk kell az ilyen nagyon vicces eseteket is. A tételmondat tehát:

Sose bízz meg abban amit a felhasználó begépel.

Mert mindig szétkúrják amit csinálsz.

Igazából a valóságban többnyire nem szándékosan rontják el a másik játékát, hanem mondjuk azért mert olyan formátumba írták be az adatot amire nem voltunk felkészülve. A tört számok elfogadása teljesen reális igény lenne a mostani programunktól, mi mégis feltételeztük hogy egész számot kapunk.

Kiírás

Sokat használtuk (és még fogjuk is) már a print() függvényt, de nem volt még rendes bemutatkozó fejezete, úgyhogy akkor most csak róla fogunk beszélni.

A print() egy olyan függvény, ami kiír mindent a standard outputra, (stdout, standard kimenet) amit argumentumul kap. Amikor konzolból fut a programunk, akkor ez a kimenet alapértelmezetten maga a konzol, hiszen ott jelenik meg a szöveg.

Nem véletlenül vezettem be az stdout fogalmát: bár egy jó ideig minden kiírás a konzolban jelenik meg, át lehet állítani, hogy a standard kimenet egy fájl legyen, vagy a memória, és ez tök izgi.

Ha a print() a standard outputra ír, akkor az input() a standard inputról olvas? Igen, sőt, létezik egy hibakimenet is, amit standard errornak hívnak. Ezek hárman az stdout, stdin és az stderr.

Azt hogy egy egyszerű kifejezést hogy írunk ki már nem mutatom be. Vegyünk néhány érdekesebb dolgot inkább.

Több érték kiírása

Eddigi példáinkban a print() mindig egy értéket kapott argumentumul (és ha például egy szöveget és utána egy számot akarunk kiírni, akkor ezeket először valahogy mindig egymáshoz illesztettük), de ez a függvény tetszőleges mennyiségű argumentum fogadására képes, és ezeket mind egyszerre, szóközzel elválasztva ki tudja írni a képernyőre. Ahogy minden más felsorolást is a Python nyelvben, úgy az argumentumokat is vesszővel elválasztva kell felsorolni.

Tekerjünk vissza a számológépes példára és írjuk át ennek szellemében:

a = int(input('Első szám: '))
b = int(input('Második szám: '))
c = a + b
print('Az összeg:', c)

Az utolsó sor változott csak, és szerintem sokkal természetesebb is így a forráskód: a print() először írja ki a szöveget, aztán a c változó értékét. Magától, impliciten str-é változik mindenki.

Ha lefuttatjuk ezt a programot, pontosan ugyan úgy jelenik meg minden, mint az eredeti példa esetén.

Kiírás ugyan abba a sorba

A print() a neki adott értékeken kívül a dolga végeztével mindig kiír egy láthatatlan vezérlőkaraktert, ami miatt a kurzor a konzolban új sorba ugrik, annak is az elejére (tehát olyan, mintha a print() “nyomna egy Enter-t”). Ezt a karaktert ezerféleképp hívják, angolul new line vagy line feed, magyarul soremelés vagy sortörés. (Ebben a bejegyzésben ennyit elég is tudni róla.)

Ha nem akarjuk hogy a print() a kiírás végén új sort kezdjen, akkor felül kell definiálni ezt a karaktert. Nagyon egyszerű, így néz ki:

print('Első szám: ', end='')
a = input()

Bizony, a print() zárójelei közé felsorolást kell írni, és a legutolsó helyen az end=''-nek kell állnia. Ez az end pont úgy néz ki mint egy változó, viszont sehol sincs definiálva. Semmilyen varázslat nem történik itt, az end valójából a “print()-ből jön”, ahhoz tartozik (keyword argumentnek vagy opcionális argumentumnak hívják). Ennél többet nem is kell egyelőre tudni ennek a működéséről.

Ne zavarjon az end='' abban hogy tetszőleges felsorolást írja a print()-be, csupán arra figyelj, hogy a felsorolás legvégén álljon ez az argumentum.
Így eu például teljesen rendbe van: print('Az összeg:', c, end='')

Azt is vegyük észre, hogy az end='' azt jelenti, hogy az end argumentum értékét üres stringre állítjuk a függvény számára.

Van visszatérési értéke a kiírásnak?

Ha minden függvénynek van visszatérési értéke, és az input() esetén ez a begépelt szöveg, akkor egyébként a print() függvénynek mi a visszatérési értéke?

Nincs semmi különös visszatérési értéke, csak a semmi, azaz a None. Írjuk ki:

print(print('Hello World'))

Először megjelen a Hello World majd alatta a None, ami ugye azért történt, mert először a belső kifejezés értékelődött ki, ami kiírta a szöveget a standard outputra, és visszatért a None-al. Ezt pedig a külső kifejezés kiírta.

Zárás

Zárásképp írjuk át a számológépet úgy, hogy az Összeg: felirat helyett jelenjenek meg a begépelt számok, és a műveleti jelek is.

a = int(input('Első szám: '))
b = int(input('Második szám: '))
print(a, '+', b, '=', a + b)

Futtassuk:

Első szám: 5
Második szám: 6
5 + 6 = 11

Szép ez meg jó ez, de eskü’ több a körítés a print()-ben mint a szöveg amit ki akarunk írni. Rövidesen visszatérünk erre a problémára, és imádni fogjátok a megoldást, de a következő részben végre megtornáztatjuk a számítógépet, mert elkezdünk vezérlési szerkezeteket írni.

Ez a bejegyzés a Python tutorialom egyik része. Az összes rész listája itt fellelhető.

-slp

2013. március 3., vasárnap

Gamma korrekció Javascript canvason

TL;DR Live demo elérhető itt: http://jsfiddle.net/slapec/m8RN9/6/
Jó, rögtön a 7. bejegyzésem (ezen a blogon) egyből nem a pythonnal foglalkozik.

A korábbi anaglif kép előállító scriptem írása közben vettem csak észre, hogy a weboldalon ahol különböző módszereket felsorolják, az optimized anaglyph eljárásnál a kész kép piros színcsatornájára gamma korrekciót alkalmaznak. Ekkor derült csak ki, hogy a PIL-ben nincs erre dedikált metódus, szóval magamnak kellett leprogramozni. Ezzel véget is érhetett volna a móka, de valójából a pythonos kódom egy javascriptes projektből származik, amit akkor mondjuk nem mondtam, de mivel kiderült hogy a javascript canvas se ismeri ezt a funkciót, ezért most felfedhettem az eredeti szándékomat.

Jó pár órával nappal ezelőtt ültem le megírni a kódot amit itt most be szeretnék mutatni. Ez a sok idő elég volt arra, hogy kicsit elkanyarodjak a probléma "magjától", szóval 2 tonna csicsa került a gyakorlatilag 3 sornyi hasznos kód köré, de nem baj, imádom a csicsát :3. Lássuk hogy mit akartam:
Képszerkesztő programokban szokott lenni külön ablak, ahol gamma korrekciót lehet állítani, csinos kis csúszkákkal, és még a függvény képe is látszik 0 ≤ x ≤ 1 között, illetve azonnal lehet látni a végeredményt is.

Arról hogy mi a gamma korrekció az első bekezdésben levő linkben lehet olvasni. Az ottani képleten egy kicsit kell kalapálni, és ezt kapjuk a végén:


Arról van szó, hogy a kapott értéket (Vin) le kell osztani 255-el, hogy 0 ... 1 közé essen. A kapott értéket hatványozzuk a gammával, ami nálam valójából a gamma reciproka, de csak azért, hogy a gamma < 1 legyen általában (ez a gamma compression) majd a hatványozott értéket vissza szorozzuk 255-el, hogy újra 0 ... 255 közé essen, hogy aztán vissza lehessen rakni a helyére.

A HTML

Annyira nem túl izgalmas, de mivel van 1-2 HTML5-ös element a kódomban, ezért akkor kicsit részletezném azt is. Ide nem másoltam be a teljes kódot, azt meg lehet nézni a bejegyzés legelső sorában levő linkjén, meg majd alul is!
<canvas id='canvas' width='512' height='512'></canvas>
<div id='controlls' style='display: inline-block'>
    <form id='gammaForm'>
        <label for='red'>Piros</label>
        <input type='range' value='1.0' min='0.2' max='5.0' step='0.1'
        id='red' name='red'></input>
        <output for='red' name='red_out'>1</output>
        <br>
        <label for='green'>Zöld</label>
        <input type='range' value='1.0' min='0.2' max='5.0' step='0.1'
        id='green' name='green'></input>
        <output for='green' name='green_out'>1</output>
        <br>
        <label for='blue'>Kék</label>
        <input type='range' value='1.0' min='0.2' max='5.0' step='0.1'
        id='blue' name='blue'></input>
        <output for='blue' name='blue_out'>1</output>
    </form>
    <br>
    <canvas id='plot' width='150' height='150'></canvas>
</div>
<img style='display: none;' id='lena' src='data:image/png;base64,'>

Az 1. sorban létrehozunk egy 512x512-es canvast.
A 2. sorban egy nagy div lesz, ebbe lesznek a kezelő szervek, pontosabban a divben levő formban. Itt a csúszkák melletti szöveg label elemek közé vannak téve, ahogy illik.
Az 5. sorban egy range típusú inputot definiáltam, amit amúgy a Firefox még nem támogat, szóval ott nem fog látszódni.
Ez egy vízszintes csúszka lesz. Az alap értéke 1.0, 0.2-ig mehet le, 5.0-ig mehet fel 0.1-es lépésekkel. Az id-je és a name-je megegyezik, mert a label-ben a for-hoz name-et kell írni... legalábbis azt hiszem ezért csináltam így.
A 7. sorban egy output element van, ebbe lesz majd beleírni a csúszka aktuális értéke. A label és az output element amúgy a weboldalon 1-1 sima szövegként fog megjelenni igazából, a különbség majd a JS oldalán lesz, illetve a böngésző pontosan tudja majd hogy mik a szándékaink.
A 23. sorig bezárólag copypasta van, de ott megjelenik még egy 150x150-es canvas, amibe majd a gamma függvény képét rajzoljuk csatornánként.
A 25. sorba egy img van, ide kerül a kép, az id-je 'lena'.
Én a src attribútumba a képet inline módon, data URI-val bemásoltam. Így a HTML fájl vagy 30kb lett, de így nem akadok bele a JS same origin policy-jába, meg a példának megteszi ez is.
Ez a kép CSS-el el lett rejtve, mert igazából nincs rá szükség, szerintem. Összehasonlítás miatt mondjuk egymás mellé lehetne tenni, de csak ennyi ötletem van.

A Javascript

Végre elérkeztem az izgalmas részhez. Először szedjük szét a feladatot részekre:

  1. Kell egy függvény ami ami benyeli a csatornánkénti (R, G, B) gammát, és módosítja a képet
  2. Kell egy függvény ami megrajzolja a gamma függvény képét
  3. Kell egy függvény ami frissíti az output elemet
(Amúgy ami a Javascriptes programozási stílusomat illeti, ez így a C-s és Pythonos élményeim egyvelege, valódi JS szakértelmet csak nyomokban tartalmaz. Na ettől még annyira nem katasztrófa, csak nem olyan elegáns)

Kép módosítása

El se hiszem, végre itt a bejegyzésem magja. Ne is várakozzunk tovább, itt a kód:
function gammaApply(gR, gG, gB) {
    var canvas, ctx, lena;
    var i, len, px, imgArray, imgData;

    var lut_r = [],
        lut_g = [],
        lut_b = [];

    lena = document.getElementById('lena');

    canvas = document.getElementById('canvas');
    ctx = canvas.getContext('2d');
    ctx.drawImage(lena, 0, 0);

    imgData = ctx.getImageData(0, 0, 512, 512);
    imgArray = imgData.data;
    len = imgData.width * imgData.height * 4;

    for (i = 0; i < 256; i++) {
        lut_r[i] = gammaCalc(i, gR);
        lut_g[i] = gammaCalc(i, gG);
        lut_b[i] = gammaCalc(i, gB);
    }

    for (i = 0; i < len; i += 4) {
        imgArray[i] = lut_r[imgArray[i]];
        imgArray[i + 1] = lut_g[imgArray[i + 1]];
        imgArray[i + 2] = lut_b[imgArray[i + 2]];
    }

    ctx.putImageData(imgData, 0, 0);
}

Hívjuk ezt a függvényt a továbbiakban gammaApply()-nak.
A 2-7. sorban deklarálok pár változót.

  • A canvas nevű mutat majd a weboldal 'canvas' nevű canvasára (ej, de szar neveket adtam, most jövök rá). 
  • A ctx-ben a contextünk lesz ezen a canvason. 
  • A lena-ba a kép lesz majd, ami a HTML-be kézzel bele lett írva.
  • Az i majd a ciklusváltozó lesz
  • A len-be a kapott kép mérete lesz
  • Az imgArray mutat majd a canvasról kihalászott kép tömbjére
  • Az imgData-ba lesz majd a canvasról kihalászott kép maga
Az 5. sorban 3 db lookup table-t is deklaráltam, minden csatornának egyet-egyet. Erre azért lesz szükség, mert nagyon sokat tud gyorsítani majd a korrekció alkalmazásánál.

A 9. sorban a lena változót ráállítjuk a képre.
A 11. sorban a canvas változót ráállítjuk a canvasra amire majd a korrigált képet rajzoljuk.
A 12. és 13. sorban először kérünk egy 2D contextet, majd rárajzoljuk a lena változóban levő képet a canvasra.
Erre a lépésre azért volt szükség, mert JS-el csak így tudjuk majd kikérni a kép bináris adatát. Arra, hogy az adatot kikérjük egy átmeneti canvast is felhasználhatnánk, de az csak plusz idő és memória. Most tehát rárajzoljuk a képet a canvasra, kikérjük az adatot, azt átírjuk majd visszaírjuk az adatot a canvasra és BUM kész is.
A 15. sorban kikérjük tehát a kép adatát, ez az imgData változóban lesz.
A 16. sorban ráállítjuk az imgArray változót az imgData .data propertijére, így egy kicsivel gyorsabb lesz az adat elérése.
A 17. sorban kiszámoljuk hogy mekkora lesz az a tömb amiben az RGB értékek benne vannak, tehát az imgArray mérete. Ez amúgy benne van az imgArray-ben, viszont így hogy ki van írva el tudom magyarázni, hogy hogy jön ki a méret.
A szélességet szorozzuk a magassággal, ez idáig rendbe van, megkapjuk hány pixel a kép összesen. Egy pixelt viszont JS-ben 4 érték ír le, a pixel R G B és A csatornájának értékei (az A az alpha), ezért kerül a 4-el szorzás a sor végére.
A 19. sorban jön az első ciklus, ez viszont még csak a LUT-ot számolja ki. A gondolat a következő:
Az 512x512-es kép mind a három színcsatornájára alkalmazunk a korrekciót, az 786,432 függvényhívás minimum, ami sok, de minden 0 ... 255 értékhez ugyan az a korrigált érték fog tartozni, tehát sokkal gyorsabb kiszámolni a 3 csatornára előre az összes korrigált értéket, hisz az csak 3 * 255 = 765 függvényhívás, és utána kikeresni a tömbből hogy majd melyik pixelhez milyen korrigált érték társul. A tömbből a kikeresés se lesz nehéz: A 0-hoz tartozó érték a tömb 0. indexén van, az 1 az 1-en ... a 255 a 255-ön.
A ciklusban elszámolunk 0-tól 255-ig, meglesz minden lehetséges érték amit egy színcsatorna felvehet. A gammaCalc() függvény megkapja a pixel értékét és a gammát és visszaadja a korrigált értéket. Ebben a ciklusban most az i változó reprezentálja a pixelt magát. Aki nem értené:
Van 1 pixelünk, ami mondjuk RGB(76, 149, 29) színű. A gammaCalc() megkapja a 76-ot és a gammát, az legyen most 1.5, kihányja hogy 113, azaz ennyi a korrigált értéke. A LUT-ban a lut_r 76. indexén a 113-as érték fog majd lenni, azaz elég lesz majd a pixel R csatornájának értékével megindexelni a lut_r-t, és készen is vagyunk.
A 25. sorban végigugrálunk négyesével a kép tömbjén, az imgArray-en. 
Így az imgArray[i] az R lesz, az i+1 a G, i+2 a B i+3 az A. A cikluson belül, ahogy feljebb írtam, megindexeljük a csatornák LUTjait a csatornák értékeivel, és amit kapunk azt állítjuk be a csatorna új értékének, szóval ez a korrigált érték kikeresése.
A 31. sorban visszarakjuk a canvas-ra az imageData objektumot, amit az előbb módosítottunk az imgArray-en keresztül, hiszen az imgArray az imageData.data-jára mutatott.

Ezzel készen is van a függvény, a lényeg készen van.

Korrekció

function gammaCalc(px, g) {
    return parseInt(Math.pow(px / 255, 1 / g) * 255, 10);
}

Ez az egyszerű kis függvény számolja ki a korrigált értékeket. A bejegyzés elején levő képlet javascriptes verziója. Annyi megjegyzés csak, hogy az egész kifejezés egy parseInt() függvénybe van téve, ami int-é castolja a korrekció eredményét. A biztonság kedvéért kapott ez a függvény egy második paramétert, ami azt jelzi, hogy milyen számrendszerű az első paramétere. Most így belegondolva, ennek csak akkor lenne haszna, ha 0-val kezdődő számokat kéne feldolgozni (az jelzi a 8-as számrendszert), de ilyen itt nem fordulhat elő. Sebaj, ez már így marad.

Függvény rajzolás

A függvény kirajzolásának nagy izgalommal estem neki, aztán végül is egy kis buta egyszerű algoritmus lett a vége. gammaPlot()-nak neveztem el, és a rajzolás maga eléggé egyszerű benne:
Vesz egy canvast, elindul vízszintesen pixelenként, és az épp aktuális pozíciójának az x koordinátáját betolja a korrekciót számoló képletbe, megkapjuk az y koordinátát, szóval mint egy sima kis függvény az általános iskolából.
function gammaPlot(r, g, b) {
    var canvas, ctx;
    var w, h, i;

    canvas = document.getElementById('plot');
    w = canvas.width;
    h = canvas.height;

    ctx = canvas.getContext('2d');

    ctx.fillStyle = '#DBDBDB';
    ctx.lineWidth = 2;
    ctx.fillRect(0, 0, w, h);

    //red
    ctx.beginPath();
    ctx.strokeStyle = '#F00';
    ctx.moveTo(0, h);
    for (i = 0; i < w; i++) {
        ctx.lineTo(i, w - (Math.pow(i / w, 1 / r) * w));
    }
    ctx.stroke();
    ctx.closePath();

    //green
    ctx.beginPath();
    ctx.strokeStyle = '#0F0';
    ctx.moveTo(0, h);
    for (i = 0; i < w; i++) {
        ctx.lineTo(i, w - (Math.pow(i / w, 1 / g) * w));
    }
    ctx.stroke();
    ctx.closePath();

    //blue
    ctx.beginPath();
    ctx.strokeStyle = '#00F';
    ctx.moveTo(0, h);
    for (i = 0; i < w; i++) {
        ctx.lineTo(i, w - (Math.pow(i / w, 1 / b) * w));
    }
    ctx.stroke();
    ctx.closePath();
}
A 2. és 3. sorban deklarálok megint pár változót, a canvasnak, a contextnek, szélességnek/magasságnak meg ciklusváltozónak.
5. sorban rámutat a kód a 'plot' canvasra
A 6. & 7. sorban lekérjük a canvas méreteit, szóval így dinamikus lesz kicsit a kód. A 9. sorban beállítjuk még a contextet.
A 11. sorban beállítom az aktuális kitöltő színt és a vonalvastagságot, majd az egész canvasra húzok egy négyzetet, így lesz egy kis háttérszín is.
A 16. sorban kezdődik az érdemi munka. A beginPath()-al kezdi rögzíteni a JS hogy majd milyen útvonalon húzza végig a tollat.
A 17. sorban beállítom a toll színét, ez most #F00
A 18. sorban lekerül a toll a bal-alsó sarokba
A 19. sorban történik a kirajzolás.
Elindul egy ciklus, ami végigszámol egyesével a vízszintes tengely mentén. A cikluson belül a lineTo()-ba állítjuk be hogy hova menjen majd a toll. Az első paramétere maga a ciklusváltozó, azaz vízszintesen mindig 1-el megy majd odébb a toll, ez az X tengely. A függőleges irányt a bejegyzés elején levő képlet alapján számoljuk, annyi a különbség, hogy nem 255-el van leosztva az i, hanem a szélességgel, hisz a lényeg az, hogy a tört 0 ... 1 közé essen. Jó, 255-el osztásnál is oda esne ha a canvas szélessége <256, de így éppen pontosan keresztbe teljesen végig ér a vonal.
A 22. sorban kirajzolódik a vonal, és a 23. sorban lezárjuk a toll mozgásának rögzítését.
Ezek után pontosan ugyan ez a kód van még kétszer lemásolva, a különbség egyedül az, hogy a különböző csatornák számolásánál más-más gammákkal fut a kirajzolás. Ezeket a gammákat a függvény paraméterül kapta, de ez látszik.

Most hogy elkészült a képet módosító függvény, és a függvényt kirajzoló függvény, már csak össze kell fogni a dolgokat.

Csúszkák kezelése

Az alábbi függvény mindig meghívódik, amikor valami változik a weboldal formjában:
function updateOutputs(form) {
    var gR = form.red.valueAsNumber,
        gG = form.green.valueAsNumber,
        gB = form.blue.valueAsNumber;

    form.red_out.value = gR;
    form.green_out.value = gG;
    form.blue_out.value = gB;

    return {
        'red': gR,
        'green': gG,
        'blue': gB
    };
}
A függvény megkapja a formot, így ebbe nem lesz getElementById..

A 2-4. sorban kiszedegetjük a csúszkák értékeit. Szerencsére van olyan properyjük, amivel azonnal számként kapjuk az értékeiket, szóval nem kell nekem castolni.
A 6-8. sorban az előbb kiszedett értékeket beállítom az outputoknak, így látszódik majd hogy melyik csúszka hogy áll
A függvény a végén visszatér a beállított értékekkel, azaz a gammákkal. Arra gondoltam hogy ha már ez a függvény úgyis mindig meghívódik, és tudja hogy ki és mit állított be, akkor vissza is adhatná, és nem kell más függvénynek magának összeszednie.

window.onload

Már csak annyi van hátra, hogy megírjuk hogy mi kerül a window.onload-ba, azaz mi fusson ha betöltődött teljesen a weboldal.
gammaApply(1, 1, 1);
gammaPlot(1, 1, 1);

form = document.getElementById('gammaForm');
form.oninput = function () {
    var g = updateOutputs(this);
    gammaApply(g.red, g.green, g.blue);
    gammaPlot(g.red, g.green, g.blue);
};

Kézzel 1,1,1-es gammákkal meghíjuk a gammaAply-t és plot-ot, így megjeleni a kép, és a függvény is kirajzolódik.
A 4. sorban kikérjük a DOM-ból a gammaForm nevű elementet, szóval a formot, na.
Az 5. sorban a form oninput eventjére beállítunk egy függvényt. Az oninput mindig lefut majd, amikor valami változik a form területén.
A 6. sorban meghívjuk az updateOutputs()-ot, ami ugye átírja majd az output elementeket és vissza is adja hogy miket állított be.
A 7. sorban a kapott gammákkal meghívjuk a gammaApply-t ami kirajzolja a képet, és a 8. sorban a gammaPlot pedig kirajzolja a függvény képét

Végeredmény

Itt látható mindenestől: http://jsfiddle.net/slapec/m8RN9/6/
Akit nem érdekel a kód: http://jsfiddle.net/slapec/m8RN9/embedded/result/

Zárás

Te jó ég! Szerintem kicsit túl sokat írok, az biztos. Ennyi elég is volt egy időre :)

-slp

2013. február 9., szombat

Anaglif 3D kép előállítása Python és PIL segítségével

Az elmúlt napokban/hetekben háromdimenziós képek előállításával foglalkoztam. Hogy egész pontos legyek, csak azzal a részével, ahol a két kép egyé lesz. A mai 3D-s tartalom legnagyobb része SBS 3D módszerrel kerül kiadásra, ez az, amikor a bal- és a jobb szemnek szánt képek 1 db képre kerülnek rá, és ennek a képnek a jobb oldalán van a jobb szemnek szánt kép, és a bal oldalán a bal. Az ilyen képek megjelenítéséhez speciális eszközre van szükség, szóval egy 3D TV-re (vagy monitorra), de szerintem gondolni kell azokra is, akik nem szeretnének ilyen eszközköbe beruházni (pl.: én). Nekik az anaglif 3D módszer lehet egy (átmeneti) megoldás. Aki nem akarja elolvasni a wikipédiás cikket: ez az a módszer amikor piros-cián szemüveggel kell nézni a képet.

Tehát most ez utóbbival foglalkoztam. Egy olyan python scriptet raktam össze, ami benyel 2 képet, és kiadja magából az anaglif eredményt. Lássuk az elméletet mögötte:

Szemüveg


A lényeg az, hogy a piros-cián szemüveg lencséi a fénynek csak egyes komponenseit engedik tovább a szem felé. A bal szemnek a piros, a jobbnak a cián színű jut, azaz annyi a feladatunk, hogy a két forrás képből egy olyan képet állítsunk elő, amin a bal forrásképnek csak a piros csatornája szerepel, és a jobb forrásképnek pedig a zöld és kék csatornája (hisz e két szín keveréke adja ki a ciánt). Ezt az egyszerű módszert hívják color anaglyphnek.

Ezeket a színcsatorna műveleteket mátrixok írják le. Nagy segítség, hogy a Stereoscopic Player készítőinek weboldalán több módszert is felsorolnak. A bejegyzés további részében az optimized anaglyph eljárás implementálását mutatom be.

Naív kód

Természetesen először körbenéztem, hogy mások hogy valósítják meg a feladatot. Szinte általános, hogy mindenki a mátrixműveletekhez a NumPy matematikai libraryt használja. Használhatnám én is, viszont azt szerettem volna, hogy minél kevesebb függősége legyen a kész scriptnek.
A képfeldolgozáshoz elengedhetetlen a Python Image Library (PIL) beszerzése, ezt nem tudtam kiküszöbölni. Szerencsére találtam egy gyors megoldást, amivel a NumPy nélkül is gyorsan elkészül az eredmény.

Az eredeti ötletem az volt, mint sok más embernek is, hogy végig haladok egy ciklussal mindkét képen, és minden pixelre értelmezem a mátrix műveleteket. Ez egy 640*480-as felbontású képnél 307,200 mátrix szorzás, aminél a számítógép általában sokkal többet is lazán elvégez, de sajnos a Python igen hamar lassúnak mutatkozik ilyen igénybevétel mellett.

Forrás képeknek a fenti 3dtv.at weblap DeAnaglyph oldalán levő képeket használtam, így könnyen le tudtam ellenőrizni hogy ugyan olyan képet generál-e az én kódom mint amilyenek a weboldalon vannak.

Lássuk a kódot:
from PIL import Image

def transform(l, r):
    #http://www.3dtv.at/Knowhow/AnaglyphComparison_en.aspx
    ml = ((0, .7, .3),
          (0, 0, 0),
          (0, 0, 0))

    mr = ((0, 0, 0),
          (0, 1, 0),
          (0, 0, 1))

    return int(ml[0][0] * l[0] + ml[0][1] * l[1] + ml[0][2] * l[2] +
               mr[0][0] * r[0] + mr[0][1] * r[1] + mr[0][2] * r[2]), \
           int(ml[1][0] * l[0] + ml[1][1] * l[1] + ml[1][2] * l[2] +
               mr[1][0] * r[0] + mr[1][1] * r[1] + mr[1][2] * r[2]), \
           int(ml[2][0] * l[0] + ml[2][1] * l[1] + ml[2][2] * l[2] +
               mr[2][0] * r[0] + mr[2][1] * r[1] + mr[2][2] * r[2])


def PIL_naive(left, right, out):
    l = Image.open(left)
    r = Image.open(right)

    pixel_left = l.load()
    pixel_right = r.load()

    for x in xrange(l.size[0]):
        for y in xrange(l.size[1]):
            pixel_right[x, y] = transform(pixel_left[x, y], pixel_right[x, y])

    cR, cG, cB = r.split()
    #Gamma correction on red channel. Value = 1.5
    cR = Image.eval(cR, lambda px: ((float(px) / 255) ** (1/1.5)) * 255)

    Image.merge('RGB', (cR, cG, cB)).save(out)

PIL_naive('sample1left.jpg', 'sample1right.jpg', 'pil_naive.jpg')

A transform() függvényben megadtam a két mátrixot, ez az ml és mr. A szorzás ki van fejtve a return utasításban, mert nem akartam még ciklusokkal is lassítani a programot. Amúgy valószínűleg még itt is lehetne kicsit gyorsítani, de ez már így marad.

A PIL_naive() függvény végzi a piszkos munkát.
A 20 & 21. sorban betöltődik a paraméterül kapott kép kép 1-1 változóba.
A 23 & 24. sorban a .load() metódus visszaad egy-egy pixel access objektumot, amivel közvetlenül bele lehet nyúlni a betöltött kép pixeleinek információiba.
A 26 & 26. sorban elindulunk függőlegesen és vízszintesen a bal oldali kép felbontása szerint. Általában a két kép ugyan olyan méretű szokott lenni, így én most a bal képnek a méretei szerint haladok.

A függvényünk szíve a 28. sorban van, itt történik a transzformáció. Létrehozhattam volna egy 3. Image objektumot, de spórolni akartam a memóriával, így egyből visszaírom a kiszámított pixel színét a jobb oldali képbe. Ez lehetne a bal is.

A 30. sorban akár be is fejezhetnénk és kiírhatnánk a képet a lemezre, de ennek a bejegyzésnek az írása közben láttam csak meg, hogy gamma korrekciót végeztek a kész kép piros csatornáján. Ezért a 30. sorban felbontjuk a már anaglif képet színcsatornákra.
A 32. sorban a a cR nevű piros színcsatorna minden pixeljére alkalmazzuk az 1.5-ös gamma korrekciót. Ehhez a pixel színének értékét leosztjuk 255-el, hogy 0 és 1 közé essen, majd elvégezzük a hatványozást az 1.5 reciprokával (így lesz világosabb a kép), majd a végén felszorozzuk 255-el a pixel színét, hogy 0 .. 255 közé essen újra az értéke.
A 34. sorban összefűzzük a csatornákat, és elmentjük a képet az out változóban levő string névvel.

Futtatjuk a kódot, és az eredmény:
Összehasonlítva az eredeti képpel, szerintem tök egyformák.

Probléma: a sebesség

Ez a cikk azért született meg, mert a fenti kód, bár teljesen nyilvánvaló hogy mit csinál, de lassú. A mai napomat arra szántam, hogy átírom úgy a kódot, hogy több processzoron fusson. Az algoritmus felbontotta volna a képet annyi részre ahány processzormag van az aktuális számítógépben, majd mindegyik mag transzformálgatja a maga részét, a végén pedig összeragasztódnak a képek.

A fenti kód az én Core 2 Duo E8400-as gépemen 1.42 mp alatt fut le. Ez talán még elfogadható. Kipróbáltam a Raspberry Pi-men, ott 32.62 mp alatt készült el egy képpel, ami azért már eléggé sok.

A 2 magos gépen a várható gyorsulás legfeljebb kétszeres, tehát 700 ms, ami egyébként 1.4 FPS. Az RPi egymagos, azaz ott nem lesz gyorsabb.

Gyors konvertálás .convert() metódussal

Miközben olvasgattam a PIL kézikönyvet, a szemem a Image objektumok .convert() metódusánál erősen elkezdett csillogni. Ez a metódus képes átkonvertálni a kép palettáját egy mátrix segítségével. Gondoltam elég ha ide betápolom az anaglif mátrixokat, akkor mivel ez egy beépített metódus, biztos valahol a mélyben natív kód végzi majd a szorzást. Ez a gondolatom valószínűleg igaz is, mivel jelentős gyorsulást értem el így. De lássuk a kódot:
from PIL import Image, ImageChops

def PIL_only(left, right, out):
    #http://www.3dtv.at/Knowhow/AnaglyphComparison_en.aspx
    l = Image.open(left).convert('RGB', (0, .7, .3, 0,
                                         0, 0, 0, 0,
                                         0, 0, 0, 0))

    r = Image.open(right).convert('RGB', (0, 0, 0, 0,
                                          0, 1, 0, 0,
                                          0, 0, 1, 0))

    cR, cG, cB = ImageChops.add(l, r).split()
    cR = Image.eval(cR, lambda px: ((float(px) / 255) ** (1 / 1.5)) * 255)

    Image.merge('RGB', (cR, cG, cB)).save(out)

PIL_only('sample1left.jpg', 'sample1right.jpg', 'pil_only.jpg')

Ez a kód sokkal egyszerűbb, és lényegesen gyorsabb is.
Az érdemi munkát a PIL_only() függvény végzi.
Az 5. sorban betöltjük a left nevű argumentumban megkapott fájlnévhez tartozó fájlt, majd átkonvertáljuk a palettáját RGB-ből az adott mátrix szerint. A mátrixhoz hozzá kell csapni még egy oszlopot. Mondjuk az nem derült ki számomra konkrétan hogy ez az oszlop mit állít, csak feltételezem, hogy az alpha értéket. Mivel JPG képpel dolgozunk, és az nem támogatja az alphát, ezért ide szerintem amúgy akármit be lehetne írni.

A 13. sorban történik az izgalmas rész. Az ImageChops könyvtár .add() függvénye a két paraméterül kapott kép színcsatornáinak értékeit összeadja. A "Chops" amúgy a channel operations rövidítése, azaz csatorna műveletek. A naív implementációban is ez történt, csak ott kiszámolta a kód az egyik kép pixelének értékét, és azonnal hozzáadta a másik kép pixeléhez. Az összeadás ott is lehetett volna külön lépés.
Ugyan ebben a sorban, a .split() metódus felbontja a képet színcsatornákra. Erre ugye azért lesz szükség, hogy elvégezzük a gamma korrekciót a piros csatornán. Ez meg is történik a 14. sorban, hasonlóan a natív kódhoz.

Ezek után már csak a csatornák összefűzése következik, a függvényen kívül pedig a függvény hívása.

Eredmények


Gyorsabb? Mi az hogy! Az E8400-on 0.062 mp, ez 22.9x-es gyorsulás, ami már 16.12 FPS. A Raspberry Pi-n 0.8 mp, ami 40,7x-es gyorsulás. Ja és a kész kép:


Pontosan ugyan úgy néz ki

Nem próbáltam ki hogy NumPy-vel gyorsabb lenne-e a naív kódom mint ami a PIL beépített .convert() metódusát használja, majd egyszer kipróbálom azt is.
Azt viszont kipróbáltam, hogy gyorsabb-e ha a gamma korrekcióban az értékeket előre kiszámolom, és utána csak egy tömböt indexelgetek, de sajnos nem értem el gyorsulást. Előfordulhat viszont, hogy több pixellel idővel megjelenik valamilyen gyorsulás.

2013. január 29., kedd

03 - Operátorok, kifejezések, utasítások

Még mielőtt akárminek is nekikezdenék, szeretném jelezni, hogy igazodva a kor igényeihez, ez a bejegyzés 2016. február 26-án újraírásra került. Az eredeti bejegyzés még a Python 2.7.3 használatával készült. Azóta gyakorlatilag az összes jelentős csomag Python 3 kompatibilis verziója elkészült, és magam is napi rendszerességgel Python 3 nyelvű fejlesztéssel foglalkozok.

Ideje folytatni az előző bejegyzésben elkezdett témát. Hogy kerek legyen a történet, foglalkozzunk kicsit az operátorokkal.

Mik azok az operátorok?

Az operátorok olyan elemi műveletek amikre a programozási nyelv képes. Amikor programot írunk, akkor ezeknek az egyszerű műveleteknek a véges sorozatát írjuk le. Ahhoz pedig hogy abból egy jól működő program legyen csupán a logikánkat kell majd használni.

Már az előző bejegyzésben is találkoztunk pár operátorral, ilyen volt a =, vagy a +. Ezek ismerősek lehetnek a matek órákról, nem véletlenül: a legtöbb matematikai művelethez létezik operátor a Pythonban. Az operátoroknak általában két kifejezéssel dolgoznak: az egyik az operátor bal, a másik a jobb odalára kerül. Ezeket a kifejezéseket operandusoknak hívják.

Nem kell a matek szótól hirtelen pánikot kapni, a programozás Pythonban nem egyenletmegoldásokról szól. Részemről, amikor programozok, vannak olyan napjaim, amikor a legbonyolultabb matematikai kifejezés amit írnom kell az egy egyszerű összeadás. A többi napon még annyi sincs.

Aritmetikai operátorok

Aritmetikai műveletek általában azok amiket számokon értelmezünk. Pythonban ez a fogalom jócskán ki van bővítve, mert objektumokon is értelmezhetőek egyes operátorok. Erről majd később beszélünk egyszer.

Lássuk a leggyakoribb operátorokat:

  • =
    Értékadás (assignment). Vele már találkoztunk az előző részben, most akkor beszéljünk kicsit a működéséről is. Az értékadás mindig jobbról-balra történik, azaz a jel jobb oldalán álló kifejezés eredménye bekerül a bal oldalon álló változóba.
  • +
    Összeadás (add). Konstansok, változók értékeit adatjuk össze vele, de összefűzést is jelent (ahogy az előző részben azt be is mutattam).
  • -
    Kivonás (substract). Erről nem is érdemes többet beszélni.
  • *
    Szorzás (multiply).
  • /
    Osztás (divide). Két int osztása egy float-ot eredményez, még akkor is, ha az eredmény amúgy kerek, nem tört (így tehát ez a való osztás, és mellékesen itt implicit típuskonverzió történt). Bár mára ez már történelem, de anno a Python 2-ben ez az operátor mindig int-et eredményezett, tehát szépen lehagyta a törtet az eredményről.
  • //
    Egész osztás (integer division). Ha ennek az operátornak mindkét oldalán int szerepel, akkor az eredménye is int típusú lesz, még akkor is, ha az eredménye amúgy nem egész. Ilyenkor a törtrész elhagyásra kerül. Tehát a 11 // 2 értéke 5. Negatív számoknál kicsit trükkösebb, mert a -11 // 2 eredménye -6. Tehát valójából az érték lefelé, a kisebb érték felé kerekítődik.
  • %
    Maradék képzés (modulo vagy remainder). Ez az operátor azt mondja meg, hogy két tört osztásának mennyi a maradéka. Aki régen osztott papíron az lehet már elfelejtette hogy mi is az pontosan, úgyhogy itt egy példa:

    7'1'3 : 2 = 356
    1 1 
      1 3
        1  <- Ezt a számot kapjuk eredményül

    A színezésre ne figyeljetek
    Mire jó ez az operátor? Például arra, hogy eldöntsük jól egy számról, hogy páros-e vagy páratlan (ha a maradék 1 akkor páratlan, amúgy páros).

  • **
    Hatványozás (power). Az operátor bal oldalán a hatványalap, jobb oldalán a hatványkitevő áll.

A fenti operátorokkal egy csinos kis számológéppé léptettük elé a Pythont.

Logikai operátorok

Mint ismeretes, a számítógépek a bűvös kettes számrendszert használják. Egyesek és nullák, régi történet. A logikai műveletekkel gyakorlatilag egyértelműen eldönthető kérdéseket teszünk fel a számítógépre, amire ő igennel vagy nemmel tud felelni, vagy inkább igazzal vagy hamissal, ami angolul True vagy False, számítosítva meg 1 és 0. Meg is érkeztünk a kettes számrendszerhez.

Az ilyen kérdésekre (hívjuk feltételeknek) kapott válaszokkal tudunk dolgozni. Nézzük milyen operátorok tudnak segíteni ebben:

  • ==
    Egyenlőség (equal). Logikai értéke True ha a két kifejezés egyenlő. Így vizsgálhatjuk hogy egy változó érték egyelnlő-e egy értékkel, és hasonlók.
  • !=
    “Nem egyenlőség” (not equal). Tehát ez az operátor arra ad választ, hogy a bal- és jobb oldala különböző-e.
  • >
    Nagyobb (greater). Avagy a jó öreg kacsacsőr. Akkor eredményez True-t, ha a bal kifejezés nagyobb mint a jobb.
  • <
    Kisebb (less). Akkor eredményez True-t, ha a bal kifejezés kisebb mint a jobb.
  • >=
    Nagyobb egyenlő. Akkor eredményez True-t, a a bal kifejezés nagyobb vagy egyenlő a jobb oldalival. A félreértések elkerülése végett, bár ez két karakter, egyetlen operátort jelöl, tehát nem lehet helyette olyat írni hogy =>, mert az nem létezik.
  • <=
    Kisebb egyenlő. Akkor eredményez True-t, ha a bal oldali kifejezés kisebb vagy egyenlő a jobb oldalival. Ugyan az igaz az írására, mint a >= esetén.
  • or
    “Vagy”. Bizony, ezt normális latin betűkkel, és nem hieroglifákkal kell írni. Akkor eredményez True-t, ha a két oldalán álló kifejezések közül bármelyik True. Ez akkor jön majd jól, ha olyan algoritmust írunk aminek a futása több dologtól is függ, de elég, ha azok közül legalább egy logikailag igaz.
  • and
    “És”. Akkor eredményez True-t, ha mindkét kifejezés az oldalán True. Ez meg majd akkor jön jól, ha olyan algoritmust írunk, aminek a futásához egyszerre mindennek klappolnia kell, tehát mindenkinek logikailag igaz értékkel kell rendelkeznie.
  • not
    “Nem”. A logikai kifejezés tagadása, negációja. A többiekkel ellentétben ez egy egy operandusú művelet, tehát ennek csak a jobb oldalára kell írni kifejezést. True-ra False-t, False-ra True-t eredményez.

Ezzel véget ért egyelőre a felsorolás, de még akadnak más operátorok is a Python nyelvben. Elég csak az előző részben említett index operátorra gondolni. Ezeket az operátorokat a rájuk jellemző adatstruktúrákkal együtt ismertetem, így lehet majd mihez kötni őket.

Ezeket az operátorokat ugyan úgy írhatjuk kifejezések közé, mint az aritmetikaiakat, de ne felejtsük el, hogy ezek mindig valamilyen bool típusú értéket eredményeznek. Lássunk egy rövid példát:

print(1 == 2)
print(1 > 0 and 1 > -1)

Ha ezt lefuttatjuk, akkor először a False majd a True üzenet jelenik meg a konzolban. Ez teljesen logikus, hisz nem igaz hogy az 1 egyenlő lenne a 2-vel, viszont az igaz, hogy az 1 nagyobb mint a 0 és az 1 nagyobb mint a -1.

Igazságtáblák

A logikai operátoroknak elég jól behatárolt, és nagyon kevés bemenete és kimenete létezik, ezért ezekett táblázatokba szokták gyűjteni, ez az igazságtábla. A fenti utolsó 3 operátor igazságtáblái ezek:

or

A B A or B
False False False
False True True
True False True
True True True

and

A B A and B
False False False
False True False
True False False
True True True

not

A not A
False True
True False

Rövidzár

Az and és az or operátorok képesek a rövidzárásra (short circuit). Ez azt jelenti, hogy ha az első operandus alapján biztos a művelet eredménye, akkor a második operandus nem kerül kiértékelésre. Nézzünk néhány példát:

True or print('Második operandus')
False and print('Második operandus')

Ha ezt a programot lefuttatjuk, akkor a konzolban semmi se jelenik meg. A szerint amit feljebb írtam, az első sorban az or első paramétere alapján már biztos hogy True lesz az eredmény, így a futás rá se ugrik a print()-re.
A második sorban, ha az and első operandusa False akkor biztos hogy False lesz az eredmény, és nem is kerül futtatásra a print().

Ez egy fontos tulajdonság, és nagyon hasznos tud lenni olyan szituációkban, amikor olyan kifejezést írunk, amikor tilos a második operandus kiértékelése akkor, ha az első nem teljesült.

Kifejezések

Az előző jó néhány bekezdésben gyakran jelent meg a kifejezés fogalma. Ennyi szöveg után ideje ezt is elamgyarázni.

A kifejezések a forráskód elemi részei. Egyesek egymásba is ágyazhatóak, tehát léteznek összetett kifejezések.

Ahogy leírjuk egy Python fájlban azt hogy:

number = 1 + 2

Úgy valójában 4 kifejezést hoztunk létre:

  1. Vettük az 1 mint konstans kifejezést
  2. Vettük a 2 mint konstans kifejezést
  3. Előállítottunk egy újabb kifejezést úgy, hogy alkalmaztuk az összeadás műveletét a két konstansra.
  4. Az előbb létrejövő kifejezés eredményét (ami az összeadás eredménye) az értékadó operatár eltárolta a number nevű változóban.

Ha ezt realizáltuk, akkor látható, hogy szinte végtelen mélységig pakolgathatóak egymásba a kifejezések, egyáltalán nem kötelező változókba pakolászni részeredményeket. Egy kérdés maradt már csak: ha egy sornyi kifejezést írok le, milyen sorrendben kerülnek kiértékelésre?

Kiértékelési sorrend

A kiértékelési sorrend (order of operations), avagy erősebb kutya baszik, olyan dolog, amivel soha többé nem találkozik az ember az általános iskola után, egész addig, amíg a Facebookon már megint fel nem bukkan egy olyan furmányos talány hogy:

7 - 1 * 0 + 3 / 3 = ?

Az emberek 74%-a hibás eredményt az erre az egyszerű kérdésre.

Akkor most tápoljuk be egy programba:

print(7 - 1 * 0 + 3 / 3)

Az eredmény természetesen 8. Hogy miért? Mert a számítógép soha nem bassza el.

A programozási nyelvekben, így a Pythonban is, a kifejezések kiértékelési sorrendje balról jobbra történik, és bentről kifelé, pont úgy, ahogy a matematikában. Egyes operátorok előnyt élveznek a többiekkel szemben. A sorrend (először a legerősebb, végül a leggyengébb):

  1. **
  2. *, /, //, %
  3. +, -
  4. <, <=, >, >=, !=, ==
  5. not
  6. and
  7. or

Így már teljesen világos hogy miért is 8 az eredmény.

A zárójel

Ahogy a matematikában, úgy a programozásban is előfordul, hogy ki akarjuk kényszeríteni a kifejezések kirétékelési sorrendjét. Ez megszűnteti az olyan problémákat is, mint hogy ki a faszom emlékezett arra, hogy a szorzás előbb következik mint az összeadás.

Zárójelet bárhol alkalmazhatunk a kifejezésben. Írjuk át a fentit hogy egyértelmű legyen:

print(7 - (1 * 0) + (3 / 3))

Ó, hát persze, 1 * 0 az 0, ezt minden hülye tudja!, 3/3 az 1. És akkor 7 - 0 + 1 az 8.

Zárójelet használni semmibe se kerül, cserébe megelőz minden félreértésből adódó problémát. Nem kell spórolni vele.

Önmarágra hivatkozó változó

Kiemelném azt az esetet, amikor az értékadás mindkét odalán ugyan az a változó szerepel.

Mi történik akkor ha leírjuk hogy:

number = 1
number = number + 1

Az 1. sorban csak deklaráltuk a változót. A második sorban viszont a változónak értékül adtuk saját magát úgy, hogy még 1-et hozzá is adtunk. Ez teljesen normális, hisz a jobb oldallal kezdődik a kiértékelés. Ekkor a number egy létező változó, az összeadás értelmezhető rá. A kifejezés értékét (ami 2 lesz) ez után felveszi a number.

Utasítások

Végére maradtak az utasítások. Ezek olyan kifejezések, amikből a forráskód egy sorában egy, és csak az az egy állhat, így tehát nem ágyazhatóak egymásban. Kiértékelésüket tekintve először mindig az utasítás jobb oldala kerül kiértékelésre, és csak legvégül az utasítás maga. Mindig egy operandusúak… már ha beszélhetünk operandusról, a szó szoros értelmében.

Elsőre ez eléggé szigorú megszorításnak tűnik, de ide tartozik az értékadás is. Az értékadás ugyanis nem ágyazható bele semmibe, és egy sorban nem lehet több értékadást egyszerre csinálni még ha úgy is tűnik mintha azt tettük volna.

Egyelőre tehát csak az értékadást, mint utasítást ismerjük, de rövidesen jönnek olyan finomságok, mint az elágazások, ciklusok utasításai, és akkor aztán végre elkezd úgy dolgozni a CPU ahogy kell.

Zárás

Most hogy ennyi mindent megtudtuk arról hogy mennyire jó a Python a matekból a következő részben végre bevonjuk a felhasználót is a játékba!

Ez a bejegyzés a Python tutorialom egyik része. Az összes rész listája itt fellelhető.

-slp

2013. január 13., vasárnap

02 - Változók, típusok

Még mielőtt akárminek is nekikezdenék, szeretném jelezni, hogy igazodva a kor igényeihez, ez a bejegyzés 2016. február 20-án újraírásra került. Az eredeti bejegyzésben még a Python 2.7.3-ba beépített típusokat ismertettem, egyébként hiányosan. Azóta gyakorlatilag az összes jelentős csomag Python 3 kompatibilis verziója elkészült, és magam is napi rendszerességgel Python 3 nyelvű fejlesztéssel foglalkozok.

Ha el szeretnénk tárolni a program futása során valami adatot a számítógép memóriájában, akkor változókat (variable) kell használnunk. A változó létrehozását deklarálásnak (declare) nevezik. Programozási nyelvtől függően lehet, hogy azt is definiálni kell, hogy milyen típusú adatot tárolnánk a változóban, mert ettől is függ a szükséges memória mérete. A Python szerencsére egy dinamikus programozási nyelv (dynamic programming language), azaz egy változó tetszőleges típusú adatot tárolhat. Viszont a Python erősen típusos nyelv (strongly typed), ami azt jelenti, hogy nem lehet csak úgy összevissza a különböző típusú adatokat egy műveletben használni.

Hétköznapibb példa lehet az, ha a változókat úgy képzeljük el, mint egy halom üres fiókot. Kinyitunk egyet, és belerakunk egy tetszőleges értéket. A fiókokat nevekkel látjuk el, így tudunk majd hivatkozni a bennük tárolt értékekre. A fiókoknak természetesen van méretük is, így akármekkora adatot nem lehet tárolni bennük.

Ebben a bejegyzésben a változók deklarálásáról, és a Python néhány beépített típusáról lesz szó.

Python típusok

Két fajtáját különböztetném meg a Python beépített típusainak. Ezek a primitívek és az objektumok. A primitív típusok egy értéket jelölnek. Ezek például a logikai értékek és a számok. Az objektumok összetett adattípusok, amik általában más, primitív típusokat kombinálnak.

Lássuk a leggyakoribb fajtákat:

A primitívek:

  • bool
    Boolean, vagy logikai érték. Ennek a típusnak pontosan két érték felel meg, a logikai igaz (True) és a hamis (False).
  • int
    Integer, vagy egész szám. Ezek tetszőleges nagyságú pozitív vagy negatív értékek, tizedesjegyek nélkül.
  • float
    Lebegőpontos szám, vagy tizedestört. Az ilyen típusú értékek néhány tizedesjegyig pontosak, és nem tudnak tetszőleges nagy egészrészt tárolni.
  • None
    A semmi. Néha bizony egy művelet végeredménye a nagy büdös semmi. Ennek a típusnak egy érték felel meg, ami stílusosan a None.

Az objektumok:

  • str
    String, vagy karakterlánc. Mindenféle szöveges érték ilyen típusú.
  • list
    Lista. Több különböző típusú adatot lehet benne egymás után felsorolva tárolni.
  • dict
    Dictionary, vagy asszociatív tömb. Ebben is több különböző típusú adatot lehet tárolni, de az adatokat saját egyedi azonosító kulccsal érhetjük el.

Azért hogy ne szabaduljon el ennek a bejegyzésnek a mérete, egyelőre a(z általam tapasztalt) leggyakoribb típusokat soroltam fel. Egy másik bejegyzésben visszatérünk még a tuple, set, bytes vagy a complex típusokra.

Most hogy tisztában vagyunk vele, hogy milyen típusú adatokkal dolgozhatunk ideje változókat is létrehozni, amibe ezek az adatok belekerülhetnek. Ahogy említettem, a dinamikus típusosság miatt az adat definiálja a változó típusát. Ha a változóba a program futása során más típusú adatot teszünk bele, akkor onnantól az lesz az érvényes.

Változók létrehozása

Változót pofon egyszerű létrehozni. Csupán annyit kell tenni, hogy leírunk egy szót, ami a változó neve lesz, és ezt egyelővé tesszük egy értékkel.

Példával sokkal egyszerűbb:

my_variable = 0

Gépeljük be a fenti sort egy új Python fájlba. Ezzel létre is hoztunk egy változót, aminek a my_variable lett a neve, és a 0 értéket tároltuk el benne. Ezt úgy is szokták mondani hogy inicializáltuk a változót.

Ha lefuttatod a fenti sort tartalmazó Python fájlt, akkor látszólag semmi se történik, mert nem jelenik meg semmi se a konzolban. A valóságban viszont létrejött a változó, megkapta az értékét, de semmi olyan utasítás nem volt ami kiírt volna akármit is, elfogyott a tennivaló is, ezért minden memóriát visszaadott a program az oprendszernek és szépen kilépett.

A változók elnevezéseinek meg kell felelniük annak a szabálynak, hogy csak az angol ABC kis- és nagybetűit, számjegyeket, valamint a _ karaktert tartalmazhatják. Ezen kívül megkötés még, hogy a változó neve nem kezdődhet számjeggyel, és nem állhat csak számokból sem.

Konkrét típusú változó létrehozása

Mivel a változó típusa a változóban tárolt érték típusával egyenlő, ezért adott értékekkel konkrét típusúvá tehetünk egy változót. A típus lehet úgynevezett explicit vagy implicit.

Explicit típusok:

  • bool := bool()
  • int := int()
  • float := float()
  • None := None
  • str := str()
  • list := list()
  • dict := dict()

A felsorolást úgy kell értelmezni, hogy ha például egy változóban int (egész szám) adatot akarunk tárolni, akkor ahhoz a változót az int()-el kell egyenlővé tenni:

my_number = int()

Implicit típusok:

  • bool := True vagy False
  • int := 0 vagy tetszőleges egész szám
  • float := 0.0 vagy tetszőleges tört szám
  • None := None
  • str := '' vagy tetszőleges szöveg, 2db aposztróf vagy idézőjel között (angolul string literal)
  • list := [] vagy tetszőleges felsorolás (angolul list literal)
  • dict := {} vagy tetszőleges kulcs-érték párok (angolul dict literal)

Ezt a felsorolást pedig úgy kell értelmezni, hogy ha például egy változóban int (egész szám) adatot akarunk tárolni, akkor a változót a 0 értékkel kell egyenlővé tenni:

my_number = 0

A fenti eseteket azért hívjuk implicitnek, mert a kézzel beírt adat alakja egyértelműen meghatározza a típust is úgy magától.

Változó értékének kiírása

Elég lehangoló hogy minden szó nélkül véget és a programunk, akármit is írunk bele, ezért egy picit előre haladunk, és megmutatom hogy hogyan lehet kiírni a konzolba egy változó értékét. Ehhez egyszerűen a print() függvényt kell alkalmazni.

my_number = 1234
print(my_number)

Ha ezt begépeljük egy Python fájlba, és lefuttatjuk, akkor a képernyőn az 1234 üzenet jelenik meg, majd kilép a program. Egyelőre elég annyit tudni tehát, hogy ha változót akarunk kiírni, akkor a print() zárójelei közé a változó nevét kell begépelni.

Primitívek

A fenti jó pár sor után bizonyára világos, hogy hogyan kell int-et létrehozni. Következzen pár rövid példa az összes típusra

bool

false_value = False
true_vale = True
int_as_false = bool(0)

Mivel kétféle logikai érték van, ezért itt rövid a példa. A False érték felel meg a hamis, és a True az igaz értéknek. A 3. sorban látható egy érdekes dolog, a 0 átalakítása logikai értékké. Ez talán még adja magát, hogy False lesz (ha kiírjuk az int_as_false értékét, akkor a képernyőn a False jelenik meg).

Vegyük ezt a példát inkább

true_or_false = bool('False')
print(true_or_false)

Ha a 'False' str-t bool-á alakítjuk, akkor mit kapunk eredményül? Meglepő lehet, de True-t.

Ennek az az oka, hogy minden tetszőleges értékhez tartozik egy-egy logikai érték is. Az ökölszabály az, hogy a 0 vagy az üres objektumok False értékűek. Minden más True.

true_or_false = bool('')
print(true_or_false)

Így jelenik meg a False a képernyőn.

int

positive = +1234
negative = -2016
print(positive)
print(negative)

Egymás alatt az 1234 és a -2016 sorok jelennek meg a konzolban. Ezen szerintem nincs sok magyarázni való.

float

one = 1.0
print(one)
print(one + 0.1 + 0.1)

Deklaráltuk a one változót, ami egy tört és az 1.0 értéket tárolja. A 2. sorban kiírtuk az értékét. A 3. sor viszont érdekes, ugyanis nem az jelenik meg a konzolban hogy 1.2 hanem hogy 1.2000000000000002. Ez nagyon szokatlan lehet, de ahogy korábban mondtam, a tört számok pontossága nem végtelen. Ahogy a kis törteket összeadjuk, összeadódik a “pontatlanság” is, és ha már kellően nagy ez az érték, akkor megjelenik a tört végén mint hiba. Az esetek nagy részében ez nem okoz gondot, hisz elég ránézni, a 16. tizedesjegynél van először hiba, de megvan az a terület (tudományos, pénzügyi), ahol ez nagyon is nem mindegy. Aggodalomra semmi ok, mert megvannak az eszközök a precíz tört műveletekre is (de ezzel jó ideig nem kell foglalkozni).

None

nothing = None

A None nem egy hosszú történet. Ez jelenti a Python-ban a “semmit”. Olyankor jöhet jól, ha szükségünk van arra, hogy létezzen egy változó, de a programban csak később teszünk valódi adatot ebbe a változóba. Vagy jelezhetünk vele hibát is (ami nem ajánlott, mert létezik rá dedikált módszer, viszont nem lehet elmenni a mellett, hogy a Pythonban beépítve is vannak olyan algoritmusok, amik akár hibát is jelezhetnek így).

Objektumok

Az objektumok, még ha egyelőre csak 3-ról is van szó, nagyon izgalmasak. Egyelőre viszont maradjunk az egyszerű dolgoknál

str

text = 'Hello World!'
text2 = "It's me speaking"
text3 = 'Magyar szöveg Python 3-ból"

Az str típusú értékeket '' (aposztróf) közé kell írni (angolul ez a string literal), ez látható az 1. sorban. Megengedett a "" (idézőjel) használata is. A '' között tetszőleges mennyiségű szöveg állhat, speciális karakterekkel, számokkal, és néhány vezérlőkarakter is megengedett.

Kilépő- és vezérlő karakterek

Vegyük azt az esetet, amikor egy str-ben aposztrófot akarunk írni. Az előző példából a text2 értékét írom át.

text2 = 'It's me speaking'

Látható ennek a kódrészletnek a színezéséből is, hogy az 'It' piros, viszont utána fekete a szöveg, és a sor végén egy árva piros aposztróf áll. Ha ezt a szöveget ilyen formában beírjuk egy fájlba, és futtatjuk, akkor SyntaxError hibát kapunk.

A probléma a miatt keletkezik, hogy egy stringet pontosan két aposztróf vagy idézőjel közé kell írni. Na most mivel a folyó szövegben volt aposztróf, így az zárta a stringet. Ami szöveg utána következik, az már kód, és a sor végén egy árva ' marad, aminek hiányzik a párja, tehát szét van burványolva az egész, nincs meg senkinek se a párja. Ezek azok a szituációk, amikor olyan karakterek vannak a stringben, amik akár forráskódnak is megfelelnek. Ezek elkerüléséhez “ki kell léptetni” (string escape) a speciális karaktereket. Ezt a \ karakterrel lehet megtenni, ami ilyenkor nem kerül nyomtatásra.

A hibás kódot így lehet javítani ezzel:

text2 = 'It\'s me speaking'

A \ a kilétető karakter, ami az utána következő egyetlen karaktert megvédi, így nem szakad meg a string. Fontos, mivel a \ mindig kiléptetést jelet, ezért ha azt akarjuk, hogy a szövegben megjelenjen ez a karakter, akkor ezt a karaktert is ki kell léptetni. Mutatom a példát:

prompt = 'C:\\Users\\'
print(prompt)

Ha ezt lefuttatjuk, akkor a konzolban megjelenik a C:\Users\ szöveg.

Ha tudjuk, hogy aposztróf lesz majd a stringben, akkor érdemes inkább idézőjelek közé írni a szöveget. Ez fordítva is igaz. Így egyszerűen csak olvashatóbb marad a forráskód.

list

A list egy felsorolt adattípus, azaz az értéke egy lista, amiben tetszőleges elemek lehetnek.

Minden programozó eljut arra a pontra, amikor felismeri hogy:

Akarok írni egy programot, és sok változó értéket akarok tárolni, de nem tudom előre mennyit. És annyira természetellenesnek tűnik deklarálni 100 változót, hogy valtozo00, valtozo01, …. . Nincs erre valami jobb megoldás?

Amikor ilyen szituációban találod magad, akkor kell list-et használni. A létrehozása nagyon egyszerű:

numbers = [1, 1, 2, 3, 5, 8, 11]
my_list = [0, [1, "eggs"], [2, "apples"]]

A numbers innentől tömb, amiben a vesszővel elválasztott számok szépen libasorban követik egymást. A my_list egy kicsit bonyolultabb már. Ebben először egy 0-ás számjegy, majd megint egy tömb van, amiben egy 1 értékű int, utána egy str következik. A tömbnek vége is van, és a my_list 3. eleme egy újabb tömb. Ez jól szemlélteti, hogy list-ben akármi, akár újabb list is lehet.

Elemek elérés

Az érdekes kérdés az, hogy oké hogy készítettem két listát, de hogyan tudom kiolvasni a lista 3. elemének az értékét? A válasz az index operátor.

numbers = [1, 1, 2, 3]
third_number = numbers[2]
print(third_number)

A fenti kód 2. sorában történik az indexelés, ami formailag annyi, hogy a változó nevének a végén szögletes zárójelek közé kell írni az elérendő indexet. A tömbök, és úgy általában a felsorolt típusok első elemének az indexe a 0. (0-based indexing). Tehát ahhoz hogy a tömb harmadik elemét elérjem, a 2. indexen levő értékre van szükségem, és ez is látható a 2. sor végén. A third_number változó pedig felveszi ezt az értéket. Tehát a numbers elemszáma nem változik.

dict

Mondjuk hogy van egy list-ünk, és ebből kellene valamilyen egyértelműen azonosítható tulajdonság alapján egy konkrét értéket kiválasztani. Ahhoz hogy ez sikerüljön olyan programot kell írni ami egyesével végignyálazza a tömb összes elemét, és ellenőrzi, hogy annál az elemnél vagyunk-e amit keresünk. Ha minden értéket végig kell vizsgálni, akkor onnantól nem kiválasztásról, hanem keresésről beszélünk, ami időigényes (a kiválasztáshoz képest). Ha ilyen problémával találkozol, akkor valószínűleg dict-et kell használnod.

Ebben a típusban saját magunk által tetszőlegesen definiált kulcsokhoz tudunk értékeket társítani. Legjobb lesz ha a list-es összehasonlítás mellett maradunk, úgy könnyű lesz megérteni.

Készítsünk egy tömböt amiben több személy nevét és születési évét tároljuk:

people = [[1990, 'Alice'], [1991, 'Bob'], [1992, 'Carol']]

A kérdés: ki született 1992-ben?

A válaszhoz, ahogy nekünk embereknek, úgy a számítógépnek is tételesen végig kell olvasnia az összes elemet.
De mondhatjuk azt is, hogy a születési év alapján akarjuk kiválasztani a személyek neveit, tehát jó lenne, ha a születési év egy kulcs, a hozzá tartozó érték pedig a személy neve lenne. Ez leírva így néz ki:

people = {1990: 'Alice', 1991: 'Bob', 1992: 'Carol'}

Eléggé hasonló a list-es példához. Az érték kikérése pedig pontosan ugyan olyan: az index operátort kell használni. Tehát ha az 1990-ben született személy nevére vagyok kíváncsi, akkor:

people = {1990: 'Alice', 1991: 'Bob', 1992: 'Carol'}
oldest = people[1990]
print(oldest)

Ha ezt lefuttatjuk, akkor az Alice felirat jelenik meg a konzolban. Fasza nem?

Típuskonverzió

A nyelv dinamikus működése miatt gond nélkül leírhatjuk azt hogy:

my_variable = 1234
my_variable = False
print(my_variable)

és a képernyőn megjelenik a False üzenet, de az erős típusosság miatt ha azt írjuk be hogy:

apples = 5 + 'alma'
print(apples)

akkor nagy lóbaszó hibaüzenet kapunk csak.

Az ilyen szituációk kezeléséről gondoskodik a típuskonverzió (type conversion vagy casting) művelete. A fenti példában egy számhoz próbáltam egy szöveget hozzáragasztani, de a képernyőn csak annyi jelent meg hogy TypeError: unsupported operand type(s) for +: 'int' and 'str', ami pont azt jelenti, hogy int-et str-el összeadni nem lehet.

'5' nem egyenlő 5-el!

Átkozottul fontos lesz azt fejben tartani, hogy mikor mi kerül a képernyőre kiíratásra. Ugyanis a képernyőn nem látszódik már, hogy egy számot, vagy egy olyan szöveget írunk ki, ami számjegyeket tartalmaz, viszont a Pythonnak ez pont nem mindegy.

Egy str-ben gyakorlatilag akármit eltárolhatunk, amit a képernyőn képesek vagyunk megjeleníteni karakter formájában, de attól, hogy egy string minden egyes karaktere egy számjegy, a Python még nem változtatja át a str-t magától int-é.

Ahhoz tehát, hogy a korábbi, 5 + 'alma' problémát megoldjuk, el kell érni, hogy az összeadás jel két oldalán azonos típusú adat legyen.

Explicit típuskonverzió

Explicit típuskonverzió az, amikor valamilyen jól látható módon jelezzük, hogy változzon meg egy érték típusa valami mássá. Ez a típuskonverzió legnyilvánvalóbb formája, de van néhány (nagyon kevés) olyan eset is, amikor a Python mégis képes magától a típusok módosítására. Ilyen például amikor egész és tört számokat adunk össze.

A konverziót függvények fogják elvégezni, amiket pár bekezdéssel feljebb már felsoroltam, az “Explicit típusok” nevű listában.

Azok a függvények, de úgy általában minden függvénye a Pythonban, írásmódjukban nagyon hasonlítanak az közönséges változókra, de van egy nagy különbség köztük: zárójellel végződnek. De a függvényekről majd később beszélünk …

Egy érték típusának az átalakításához csak írjuk be az értéket (ami természetesen változó is lehet) a zárójelek közé, pont úgy, mint ahogy a print()-el kiírtuk az értéket a képernyőre. A függvény előállít egy új értéket az alapján, amit a zárójelei között kapott, de olyan típussal, amilyen konverziót a függvény elvégez.

A sok magyarázat után itt az ideje hogy csináljunk is valamit. Ideje megoldani a problémát:

apples = str(5) + 'alma'
print(apples)

Az 1. sorban az 5-öt, ami egy int, beraktuk az str() zárójelei közé, így átalakult az 5 '5'-é, majd ezt összeadjuk az 'alma' str-el. Így az összeadás mindkét oldalán azonos típus van, és két str összeadásának az eredménye a bal- és jobb oldali string összeragasztva.

A fenti példa meglehetősen buta, hisz ezzel a lendülettel azt is írhattam volna hogy:

apples = '5' + 'alma'

és ez teljesen jogos. A valóságban igazából a változókban szereplő értékek típusát kell gyakran állítani, mert gyakori, hogy nem mi rendelkezünk róla, hogy a változóban milyen típusú érték van. Ilyen például, ha a felhasználó gépelt be valamit, vagy fájlból olvasunk.

Ennek szellemében, az alábbi eset sokkal-sokkal gyakoribb

some_variable = 5
apples = str(some_variable) + 'alma'

Mikor jön el a konverzió ideje?

Jogos lehet a kérdés, hogy mégis meddig kell megtartani egy változót egy adott típussal, és mikor kell átalakítani egy másikká. Én azt a szabályt alkalmazom, hogy ritkán használt típust minél később, gyakran használtat pedig minél előbb módosítok.

Ha egy változó értékére más típussal van szükségem, de viszonylag ritkán, akkor abban a ritka pillanatban egyszer elvégzem az átalakítást. Ha egy változó értékére más típussal nagyon gyakran van szükség, mondjuk onnantól hogy beolvastam egy fájlból, egészen addig, amíg kilép a program, akkor közvetlenül a fájl beolvasása után elvégzem az átalakítást, és a konvertált értéket használom onnantól végig.

Konstansok

Elég jól átrágtuk magunka ennek a résznek a nehezén, úgyhogy levezetésül pár szó a konstansokról.

A konstansok gyakorlatilag a változók ellentettjei. Amikor leírod hogy:

text = 'Hello World!'
print(text)

Akkor tiszta sor hogy a text az egy változó, és hogy a 'Hello World!' egy string. De ez a str egy konstans is, azért, mert kézzel egy pontos, konkrét értéket gépeltél be a forráskódba.

Sokkal jobban érződik ez, ha nem akarsz balra-jobbra tárolgatni egy stringet, csak ki akarod írni a konzolba. Az így néz ki:

print('Hello World!')

Látszik, azt a 'Hello World!'-öt nem változóból nyertük; be van írva, nem lehet megváltoztatni, az egy konstans.

Zárás

Bele akartam kezdeni a kifejezések témakörébe, de azt hiszem hogy ez a következő epizódra marad. Egyelőre viszont már tudunk változókat deklarálni, és adatokat rakni beléjük, úgyhogy jó úton haladunk, na!

Ez a bejegyzés a Python tutorialom egyik része. Az összes rész listája itt fellelhető.

-slp