2016. április 11., hétfő

08 - Hibakezelés

Hibák kezelése

Eddig tökéletes, hibátlan szoftvereket fejlesztettünk, mert ezek egyszerű kis kódok voltak, amik általában nem is függtek olyan adattól amit a felhasználótól kaphattunk. Ezért nem is nagyon volt minek elromlania.

A valódi életben viszont a valami mindig el fog baszódni. Jobb esetben a program futtatása előtt, rosszabb esetben (és általában) futás közben.

Háromféle hiba keletkezhet a programban: szintaktikai, futásidejű és szemantikai (logikai).

Szintaktikai hiba az, amikor a forráskódunk hibás: nem megfelelő a behúzás, elmarad a zárójel, vessző, és ehhez hasonló dolgok, amik megakadályozzák a Pythont hogy elolvassa a forráskódot. Az ilyen hibák nem engedik hogy elinduljon a program, viszont elég jól behatárolhatóak, mert a Python meg tudja mondani, hogy melyik volt az a sor és karakter, ahol valami szokatlan/nem várt parancs állt.

A futásidejű hiba az, amikor a programunk helyesen fut, de aztán egyszer csak valami nem várt esettel találkozik a program. Rengeteg ilyen hiba keletkezhet akkor, ha a felhasználótól várunk adatra. Például felszólítjuk az usert, hogy gépeljen be egy dátumot, de ő nem olyan formátumban teszi ezt meg amire az algoritmust írtunk. Ekkor rövid időn belül hibát jelez a Python, mert mondjuk a begépelt adatban nem annyi számjegy van mint amire számítottunk; kötőjel helyett szóköz van és ezért nem tudjuk a dátumot komponensekre szétbontani. Remélem érezhető: igen nehéz minden esetre felkészülni, de a lényeg, hogy a nem várt adat előbb-utóbb mindenképp megakasztja az algoritmust, amit a Python jó vastag hibaüzenettel ad majd a tudtunkra.

A szemantikai hiba a legtrükkösebb. Ilyenkor a forráskód helyes, viszont a program “nem azt csinálja amit akarunk” (természetesen a program mindig azt és csak is azt csinálja amit a forráskódban leírtunk). Ezeket a hibákat csak akkor tudjuk kijavítani, ha átolvassuk és újra értelmezzük az algoritmusunkat, esetleg teljesen újat írunk ugyan annak a problémának a megoldására. A lényeg, hogy szemantikai hiba miatt nem kapunk majd hibaüzenetet, ezért nagyon nehezen detektálhatóak az ilyen hibák, e miatt igen kártékonyak tudnak lenni (mert csak akkor vesszük észre hogy valami rosszul működik, mire az látható kárt okozott).

A hibákat a hétköznapokban bug-nak szokták hívni.

Szintaktikai hibák

A szintaktikai hibák száma annál kevesebb minél többet programoz az ember, ezért ezekkel csak egy példa erejéig foglalkozunk. Vegyük az alábbi hibás kódot:

print(Hello world)

A probléma hogy a Hello World nem string, mivel nincs idézőjelek közé téve. Ha futtatjuk a programot, akkor valami ilyesmit kapunk a konzolba:

  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 1
    print(Hello world)
                    ^
SyntaxError: invalid syntax

Pythonban ezt a szöveget traceback-nek nevezik. A következő fejezetben részletesen át is nézzük a részeit. Egyelőre viszont elég az utolsó sort olvasni, mert többnyire itt van a lényeg. Ez az esetünkben a SyntaxError: invalid syntax szöveg. Igen csekély angol tudással is látható, hogy a hibát a helytelen szintaxis okozza. Soronként felfelé haladva látszódik az a részlete a forráskódnak, ahol a hiba érzékelhetővé vált, majd legfelül az is, hogy melyik fájl melyik sorában történt a hiba. Így már csak oda kell menni és ki kell javítani.

Szintaktikai hiba keletkezése

A fenti tracebackben a ^ jellel jelölték, hogy a forráskódban hol keletkezett szintaktikai hiba. Ez a jel a world szó végére mutat. Ez nem véletlenül van így: a Hello szónál nem beszélünk még hibáról, hisz a Hello idézőjelek nélkül akár értelmezhető lehetne változóként is. Viszont a függvény argumentumai között vesszőnek kell állnia, ami hiányzik. A forráskód értelmezése eljut függvény hívásának végéig, így biztossá válik, hogy már nem is lesz vessző az argumentumok között, így tehát a world szó az, amire nem számít a Python, és létrejön a SyntaxError.

A mesének az a lényege, hogy a Python mindig azt a pontot mutatja meg, ahol már biztos hogy hibás a szintaxis. Előfordulhat, hogy ez sorokkal később válik csak nyilvánvalóvá:

print(int('10')

Itt lemaradt a print() bezáró zárójele. A következő tracebacket kapjuk:

  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 2

                   ^
SyntaxError: unexpected EOF while parsing

Bizony, ez a nagy büdös semmire mutat. Az unexpected EOF while parsing üzenet azt jelenti, hogy korábban lett vége a fájlnak mint amire a Python számított (Az EOF az End Of File kifejezés rövidítése).

Ez azért történhetett, mert a Python a print() lezáró zárójelét keresete, de közben véget ért a fájl, így az már sose lesz meg.

Szemantikai hibák

A szemantikai (logikai) hibák úgy kerülhetőek el a legkönnyebben, ha nem írunk rossz kódot. Na persze, ezt aztán baromi könnyű mondani, de megcsinálni azért már nem annyira.

Gondos tervezéssel és megfelelő teszteléssel kellően jól megbizonyosodhatunk róla hogy az algoritmusunk jól működik. A forráskód tesztelésére is kínál eszközöket a Python, de egyelőre nem foglalkozunk ezeknek a használatával, mert arról megint egy jó vastag bejegyzésre való szöveget össze lehetne hordani.

Egyelőre tehát igyekezzünk jó algoritmusokat írni, és keressük a hibákat végig a fejlesztés során. Azaz készüljön el a program egy kisebb része, próbáljuk ki hogy megfelelően működik-e, majd folytassuk az építkezést. Így még a program alapjainál kiderülhet ha valami rosszul működik.

Ha beütött a baj, és látszólag minden megfelelőnek tűnik, akkor jöhetnek az úgynevezett debugger programok. Pythonban ilyenkor a program futása minden soron megáll, és megvizsgálhatjuk a memória tartalmát, láthatjuk hogy a program egy elágazásban milyen irányba fog haladni, és egyéb érdekes dolgok.

Szemantikai hibák keletkezése

Vegyünk egy tipikus logikai hibát, amitől a programunk végtelen ciklusba fog kerülni:

i = 0
while i != 10:
    print(i)

A probléma az, hogy elfelejtettük növeli az i változó értékét, így sosem fog a program kilépni a ciklusból.

A fenti példa még elég triviális, a következő viszont már nem biztos:

i = 0
while i != 10:
    print(i)
    i += 0.1

Ebben a kis kódban 0.1-esével növeljük az i változót, amíg az el nem éri a 10-et. Valamiért azonban mégis átugorja a Python a feltételt és elkezd számolni mint a barom felfelé.

Ha sikerül elkapni az első pár számot ami a konzolba kerül, akkor talán beugrik valami:

0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
1.0999999999999999

A float típusnál említettem, hogy a lebegőpontos számok pontossága nem végtelen. Pontosan e miatt történt a baj is: összeadódnak a törtek hibái, és ezért az i változóban levő tört sose lesz majd pontosan 10.

Futásidejű hibák

Igazából az egész bejegyzésnek a fő témája a futásidejű hibák kezelése.

A programozás során kétféleképp győződhetünk meg róla, hogy nem okoz-e hibát az algoritmusunk: először megnézzük hogy szabad-e elvégezni a műveletet és utána végrehajtjuk, vagy először végrehajtjuk és utólag kezeljük a hibát ha keletkezett.

Az előbbi módszert LBYL-nek (Look before you leap (Először gondolkodj, aztán cselekedj)), az utóbbit EAFP-nek (Easier to ask for forgiveness than permission (Könnyebb bocsánatot kérni mint engedélyt)) nevezik.

A Python nyelvben mindkét filozófia megvalósítható, de a nyelv készítői az utóbbi megoldást javasolják.

Személy szerint eléggé keverten használom a két módszert.

Futásidejű hiba keletkezése

Vegyük a korábban felvázolt problémát: kérjük be a felhasználótól a mai dátumot a következő formátumban: ÉÉÉÉ-HH-NN (év, hónap, nap, számokkal, kötőjellel elválasztva), és írjuk vissza a konzolba a magyar helyesírásnak megfelelően.

months = {
    '01': 'január',
    '02': 'február',
    '04': 'március',
    '05': 'április',
    '06': 'június',
    '07': 'július',
    '08': 'augusztus',
    '09': 'szeptember',
    '10': 'október',
    '11': 'november',
    '12': 'december'
}

today = input('A mai dátum: ')

parts = today.split('-')
year, month, day = parts
month_text = months[month]

print('{0}. {1} {2}.'.format(year, month_text, day))

Ahhoz hogy a számjegyekkel felírt hónapot magyar szavakra átalakítsuk felsoroltam az összes hónapot a months dict-ben. Arra számítok hogy ha az aktuális hónap egy számjegyű akkor a bal oldalára nullát írnak majd. A kulcsok azért str-ek és nem int-ek, mert így megspóroltam a begépelt hónap int-é alakítását.

Akkor futtassuk a programot és íruk be pl hogy 2016-04-10:

A mai dátum: 2016-04-10
2016. március 10.

Ez bizony elég szépen ment. Akkor most gépeljük be a dátumot de kötőjelek helyett pontokkal, így: 2016.04.10.

A mai dátum: 2016.04.10.
Traceback (most recent call last):
  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 18, in <module>
    year, month, day = parts
ValueError: not enough values to unpack (expected 3, got 1)

Meg is érkeztünk, ez bizony elég szépen meghalt. Kezdjük a tűzoltást a hibaüzenet értelmezésével.

A traceback értelmezése

Szerintem a Python nagyon jó és informatív üzenetet ad hiba esetén. Végigkövethető benne a hiba teljes útvonala egész addig amíg az össze nem omlasztotta a programot.

A traceback két részből áll: a legutolsó sora mutatja a hiba típusát, és a hozzá tartozó rövid hibaüzenetet. A fenti példában tehát ez az:

ValueError: not enough values to unpack (expected 3, got 1)

A hiba típusa a kettőspontig tart, ami ebben az esetben a ValueError. Ez nem csak egy egyszerű szó, hanem egy konkrét Python osztály megnevezése, de egyelőre még hívjuk csak típusnak. Több ilyen típus is található beépítve a Pythonban.

A kettőspont után következik maga a hibaüzenet. Ez általában valamilyen angol mondat, nálunk most ez: not enough values to unpack (expected 3, got 1).

Az üzenet azt jelenti, hogy nincs elég elem amit unpack-elni lehetne (3 kéne de csak 1 van). Ezt a problémát mindjárt orvosoljuk, de előbb vegyük a traceback többi részét.

A hibaüzenet feletti sorok ezek voltak:

Traceback (most recent call last):
  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 18, in <module>
    year, month, day = parts

Itt látható hogy melyik fájlban ("/home/slapec/.PyCharm2016.1/config/scratches/blog.py"), melyik sorban (18), melyik modulban (<module>) történt a hiba, alatta pedig az a konkrét sora a forráskódnak, ahol a hiba létrejött.

A <module> ebben az esetben azt jelenti, hogy közvetlenül a folyó forráskódban volta a hiba.

Ez egy elég rövid kis traceback volt, mivel a hiba nem valamelyik függvény (vagy más egyéb struktúra) mélyén keletkezett. Írjuk át kicsit a kódot, hogy vastagabb hibaüzenetet is lássunk:

def get_month(month):
    months = {
        '01': 'január',
        '02': 'február',
        '04': 'március',
        '05': 'április',
        '06': 'június',
        '07': 'július',
        '08': 'augusztus',
        '09': 'szeptember',
        '10': 'október',
        '11': 'november',
        '12': 'december'
    }

    return months[month]

def pretty_date(today):
    parts = today.split('-')
    year, month, day = parts
    month_text = get_month(month)

    return '{0}. {1} {2}.'.format(year, month_text, day)


today = input('A mai dátum: ')
print(pretty_date(today))

Annyi változtatás történt, hogy a hónap átalakítása, és a magyar formátumra alakítás egy-egy függvénybe került (get_month() és pretty_date()). Vegyük észre, hogy a pretty_date() hívja majd meg a get_month()-ot.

Most írjuk be a dátumot például így: 2014-4-10.

A mai dátum: 2016-4-10
Traceback (most recent call last):
  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 27, in <module>
    print(pretty_date(today))
  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 21, in pretty_date
    month_text = get_month(month)
  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 16, in get_month
    return months[month]
KeyError: '4'

Ez rögtön érdekesebben néz ki. A hiba típusa most a KeyError. Ilyet akkor kapunk ha nem létező kulcsot próbálunk elérni egy dict-ben. A hibaüzenet egyszerűen annyi hogy '4'. Így össze is áll a kép: a '4' kulcs valahonnan hiányzik. Mindjárt az is meglesz hogy honnan.

Láthatóan 3 sor tartalmaz fájlnevet, de ezek mind ugyan arra a fájlra mutatnak. Általában elmondható, hogy ezeket a sorokat lentről-felfelé érdemes olvasni. Ha így haladunk, akkor legelőször azt a sort látjuk, ahol ténylegesen létrejött a hiba. Ahogy haladunk felfelé, úgy követhetjük hogy a hiba hogyan gyűrűdzik végig a programon.

  • A program a 16. sorban létrejött a hiba a return months[month] utasításnál. Ez a get_month modulban (függvényben) van. Így már tiszta is, hogy a months dict-ből hiányzott a kulcs.
  • Ide úgy jutottunk, hogy a 21. sorban a month_text = get_month(mont)-nél meghívtuk a hibázó függvényt. Ez a pretty_date modulban (függvényben) történt.
  • A 21. sorba pedig a 27. sorból ugrottunk. Ez a <module> modulban van, tehát minden függvényen kívül.

Jól látható, hogy maga a hiba igen mélyen történt, az mégis egészen addig halad felfelé a programban, amíg a program ki nem lép.

A hiba kezelése az a művelet lesz, amikor ezt a hibát az útja során valahol elkapjuk, hogy ne omlassza össze a programot.

A hiba keletkezését hétköznapian úgy mondjuk hogy az algoritmus hibát dob. Angolul, a Pythonos elnevezéseknek megfelelően ezt úgy mondják, hogy raise.
Az egyértelmű hogy a raise nem azt jelenti hogy dobni. Más programozási nyelvekben a raise helyett a throw kifejezést használják és ennek a szónak a magyar változata épült be a beszédbe.
Hasonlóan, a hibát elkapni fogjuk. Ennek a Pythonos elnevezése az except lesz, ami szintén köszönő viszonyban sincs az elkapni kifejezéssel. Ugyan úgy, az eltérő programozási nyelvekben használt catch kifejezés vált elfogadottá a beszédben.

A hiba kezelése

Azon a ponton ahol létrejött a hiba véget ér a kód futása, és egy hiba objektum (exception) indul felfelé a programban. Ilyenkor különleges állapotba kerül a Python: ha nem kapjuk el a hibát, akkor a hibakezelő algoritmusok kivételével semmilyen kód sem fut tovább, csak az objektum halad felfelé. Ha sehol se kaptuk el a hibát, akkor végül mindenképp kilép a program (így látszódik majd minden sor a traceback-ben amin végighaladt a hiba).

Az algoritmusunkat (illetve annak tetszőleges részét) úgynevezett try blokkba rakva jelezhetjük a Pythonnak, hogy a blokkban előfordulhat hiba. Ha a blokkban hiba történik, akkor a futás automatikusan a hibakezelő ágakra kerül. Itt típusonként különböző módon foglalkozhatunk a hibával. Ha nem volt olyan ág, ahol a hibát lekezeltük volna, akkor a hiba halad tovább felfelé.

A korábbi traceback-ben három sor is látható volt, így jól megbecsülhető, hogy három helyen kaphattuk volna el a hibát. Az egyszerűség kedvéért most a program végén tesszük ezt meg. Lássuk az új struktúrát!

def get_month(month):
    months = {
        '01': 'január',
        '02': 'február',
        '04': 'március',
        '05': 'április',
        '06': 'június',
        '07': 'július',
        '08': 'augusztus',
        '09': 'szeptember',
        '10': 'október',
        '11': 'november',
        '12': 'december'
    }

    return months[month]

def pretty_date(today):
    parts = today.split('-')
    year, month, day = parts
    month_text = get_month(month)

    return '{0}. {1} {2}.'.format(year, month_text, day)


today = input('A mai dátum: ')

try:
    print(pretty_date(today))
except KeyError:
    print('Hibás hónap!')

Semmi sem változott, az utolsó 4 sor kivételével:

  1. A try-al kezdtük az új blokkot.
  2. Hiba esetén az except sorra ugrik a program. Az utasítás mellé a hiba típusa kerül, aminek bekövetkeztében a program futásának itt kell folytatódnia. Látható, hogy a KeyError nem string! Másik fontos dolog, hogy ide nem lehet feltételeket tenni, mint az if esetében.
  3. Az except ágba kerül az a kód, ami KeyError hiba esetén lefut majd.

Futtassuk így a programot, és írjuk be hogy: 2016-4-10. Így már a Hibás hónap! felirat jelenik meg a konzolban. Ez egyből sokkal barátságosabb.

Most próbáljuk meg a legelső hibás példa szerint begépelni a dátumot, azaz pontokkal elválasztva. Ekkor még mindig hibaüzenet kapunk, mivel ez nem KeyError típusú hiba volt.

A try-hoz tetszőleges számú except adható, így kezelhetjük a ValueError-t is:

try:
    print(pretty_date(today))
except KeyError:
    print('Hibás hónap!')
except ValueError:
    print('Hibás formátum!')

Innen már egyszerű: ha a try-ban KeyError történt akkor az az alatti ágban; ha ValueError keletkezett akkor meg annak az ágába folytatódik a program.

Lehetséges egy ágban lekezelni több különböző típusú hibát is a típusok felsorolásával:

try:
    print(pretty_date(today))
except (KeyError, ValueError):
    print('Hibás dátum!')

Bár úgy nézhet ki a zárójel miatt mintha az except egy függvény lenne, de az továbbra is utasítás mard. Az extra szóköz nyomatékosítja ezt.

A fenti kóddal általánosságban elmondhatjuk, hogy ha a pretty_date() akár KeyError akár ValueError hibát dob, akkor hibás volt a dátum amit begépelt a felhasználó.

Az else ág

Írjuk át a programot így:

try:
   text = pretty_date(today)
except (KeyError, ValueError):
    print('Hibás dátum!')

print('A dátum:', text)

A szándék az volt, hogy a try-ban csak a dátum konvertálását végezzük el. Ha ez sikeres volt, akkor a kiírás is biztos menni fog, így annak felesleges try-ban maradnia.

Ha helyes dátumot írunk be, akkor minden a számításaink szerint zajlik majd. De ha hibásan:

Hibás dátum!
Traceback (most recent call last):
  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 33, in <module>
    print('A dátum:', text)
NameError: name 'text' is not defined

Akkor csak egy jóféle NameError exceptiont kapunk csak. Ez azért történt, mert a text változót a try ágban deklaráltuk. A hiba miatt viszont a deklaráció nem történt meg, így a try-on kívül nem létező változót próbáltunk meg kiírni.

Ezt többféleképp is elkerülhetjük, de talán a legjobb megoldás az, ha az else ágát használjuk:

try:
   text = pretty_date(today)
except (KeyError, ValueError):
    print('Hibás dátum!')
else:
    print('A dátum:', text)

Az else igazából az except-el van kapcsolatban: KeyError vagy ValueError esetén fusson az except ág, különben fusson az else ág. Tehát ebben az ágban van az a kód, amivel folytatni kell a programot ha nem volt hiba.

A finally ág

Az utolsó ág ami a try-hoz tartozik a finally. Az ebben álló kód mindenképp lefut amikor a program futása elhagyja a try blokkot, akár volt hiba, akár nem.

Ide általában cleanup kódot szoktak tenni, például bezárják a nyitott fájlokat, vagy kiírnak valami üzenetet mielőtt elhalálozik a program.

Mivel még nem tudjuk hogy hogyan kell fájlba írni, ezért innen most hiányzik a példa.

A nagyon nem várt hiba

A fenti algoritmust végülis egész jól körbebástyáztuk. A komplex programokban könnyen előfordul, hogy bár egy csomó esetre felkészültünk, valamiért mégis olyan hibát kapunk, amire aztán egyáltalán nem számítottunk.

A beépített hiba típusok hierarchikus elrendezésűek, azaz a hierarchia alján álló típusokra minden igaz ami a felettük álló típusokra (öröklik azok tulajdonságait). Ennek a hierarchiának a tetején az Exception típus áll. Azaz ha ezt a típusú hibát kapjuk el, akkor tényleg minden hibát le tudunk kezelni:

try:
    print(pretty_date(today))
except Exception:
    print('Hibás dátum!')

Opcionálisan így rövidíthető a fenti kód:

try:
    print(pretty_date(today))
except:
    print('Hibás dátum!')

Egyes esetekben elvárt, hogy a program semmilyen körülmények között se álljon le, viszont az ilyen hibakezeléssel azt kockáztatjuk, hogy az amúgy igen súlyos fennakadásokról sose kapunk majd tájékoztatást.

Saját hiba készítése

Nem csak a Pythontól várhatunk hibákat, hanem saját magunk is eldobhatunk exception-öket amiket valahol máshol elkaphatunk. Ez többek között függvényekben jöhet nagyon jól: így hibával jelezhetjük, ha nem megfelelő értékeket kaptunk.

Írjunk egy nagyon egyszerű kis függvényt, ami kizárólag pozitív számokat adhat össze:

def add(a, b):
    if a >= 0 and b >= 0:
        return a + b
    else:
        raise ValueError('A paramétereknek 0-nak vagy annál nagyobbnak kell lenniük')

Az 5. sorban látható az újdonság, a raise. Ezzel az utasítással egy exception objektumot dobhatunk el, magyarul így tudunk hibát létrehozni. Itt ugyan azt a ValueError-t használtam, mint amit korábban a hibás formátumú dátumnál elkaptunk.

A ValueError-t majdnem úgy kell használni mint egy függvényt, csak ez igazából egy osztály. Régóta kerülgetjük már az osztály fogalmát, pár rész és végre tényleg megdumáljuk hogy mik is azok valójában.

Ha megfelelő, tehát 0 vagy annál nagyobb argumentummal hívjuk meg a függvényt akkor minden rendben lesz. Ha valamelyik argumentum viszont negatív, például így:

add(-5, 10)

Akkor ezt a csinos hibaüzenetet kapjuk:

Traceback (most recent call last):
  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 8, in <module>
    add(-5, 10)
  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 5, in add
    raise ValueError('A paramétereknek 0-nak vagy annál nagyobbnak kell lenniük')
ValueError: A paramétereknek 0-nak vagy annál nagyobbnak kell lenniük

Őszintén megmondom, igen csinos ez a kis üzenet. Innentől már csak rajtunk áll, hogy hol-milyen hibákat akarunk eldobálni.

Természetesen nem csak a ValueError használható; itt van a korábban látott KeyError is, és még egy csomó más típust tartalmaz beépítve a Python, de a legjobb hír az, hogy később majd mi is fogunk teljesen saját hiba típusokat létrehozni.

Az üres utasítás

Levezetésnek következzen egy új utasítás, amiről idáig nem volt szó: a pass.

Pythonban nem hagyhatunk üresen blokkokat, de van amikor mégis erre lenne szükség: például hiba esetén egyszerűen ne csináljunk semmit. Ekkor használható a pass utasítás. Ez az utasítás a szó szoros értelmében nem csinál semmit, csak járattatja a processzort. Tölteléknek használható.

Vegyük elő újra a dátum beolvasós példát. Írjuk át úgy, hogy ha hiba történik, akkor szépen csöndben lépjünk ki minden felhajtás nélkül:

def get_month(month):
    months = {
        '01': 'január',
        '02': 'február',
        '04': 'március',
        '05': 'április',
        '06': 'június',
        '07': 'július',
        '08': 'augusztus',
        '09': 'szeptember',
        '10': 'október',
        '11': 'november',
        '12': 'december'
    }

    return months[month]

def pretty_date(today):
    parts = today.split('-')
    year, month, day = parts
    month_text = get_month(month)

    return '{0}. {1} {2}.'.format(year, month_text, day)


today = input('A mai dátum: ')

try:
    print(pretty_date(today))
except (KeyError, ValueError):
    pass

A legutolsó sorban van a lényeg. A pass akárhol használható egyébként (függvényben, ciklusban, stb) de csak akkor van értelme használni, ha önmagában áll, máskülönben akármilyen más utasítás behelyettesíthető a helyére.

Függvényben akkor szokták használni, ha a függvény neve már megvan, de az algoritmus még nincs. Ilyenkor hogy a függvény hívható legyen egy pass utasítást tesznek a törzsébe és később írják meg a kódot a helyére.

Utolsó példa

Írjuk át úgy a fenti kódot, hogy a program próbáljon meg újra dátumot beolvasni ha hiba történt.

while True:
    today = input('A mai dátum: ')

    try:
        print(pretty_date(today))
        break
    except (KeyError, ValueError):
        print('Hibás dátum!')

A végtelenségig várjuk a begépelt szöveget és majd a 6. sorban ugrunk ki a ciklusból. Az 5. sorban megpróbáljuk átalakítani a dátum átalakítását. Ha ezen a ponton hiba történik akkor a futás az except ágra ugrik. Itt nem állítjuk meg a ciklust, így az újra indul majd.

Ha viszont nem volt hiba, akkor a dátum kiírása után a program 6. sorral folytatódik, ahol a break utasítás kiléptet a ciklusból. Ilyen egyszerű.

Zárás

Azt hittem hogy ez a bejegyzés nem lesz bazi hosszú, és hogy még a modulokra is lesz idő. Ehhez képest már majdnem 20.000 karakternél járok, úgyhogy jobb is itt befejezni ezt a témát.

A következő részben tényleg jönnek a modulok, és remélhetőleg végre fájlba is fogunk írni.

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

-slp

2016. március 30., szerda

07 - Függvények

Ahogy ígértem, ma a Python lelkivilágával és a függvényekkel fogunk foglalkozni, tehát azzal hogy mégis mi az a print() vagy az input(), hogyan működnek, na meg hogy hogyan írhatunk hasonló dolgokat.

Mi az a függvény? Miért akarunk függvényt írni?

Az eddigi tudásunkkal az úgynevezett struktúrált programozói szemlélet szerint írtunk programokat. Itt az utasításokat, elágazásokat és ciklusokat kombináltuk. Könnyen előfordulhat olyan szituáció azonban hogy azon kapjuk magunkat, hogy a időről-időre a régebbi kódsorainkat illesztjük be az új programunkba, esetleg módosítunk benne pár apróságot, például a változók neveit, de a logika azonos marad.

Egy saját függvény megírása pontosan erre a problémára jelenthet megoldást: a hosszú, összefüggő forráskódból kivesszük a logikailag összetartozó funkciókat, és elhelyezzük őket egy függvénybe. Ha ezzel megvagyunk, akkor a program írása során minden esetben amikor ugyan arra a funkcióra van szükségünk, akkor meghívjuk a függvényünket.

A cél tehát a forráskód újrahasznosíthatósága, amire egy eszköz a függvények definiálása. Az újrahasznosítás mellett a függvények elfedik az bennük rejlő algoritmus részleteit, működését is: senkit nem érdekel hogy mégis milyen kód fut le akkor amikor kiadjuk hogy print('Hello World'), csak azt tudjuk, hogy a print() függvény a standard kimenetre kiírja az argumentumait és nem tér vissza semmivel se. Semmi gond nincs amiatt hogy nem tudjuk hogy mit csinál pontosan a Python amikor fut a függvény, mert tökéletesen tudjuk hogy milyen értékeket vár tőlünk a függvény, és milyen értékeket kaphatunk tőle, és ez nekünk bőven elég.

Természetesen amikor saját függvényt írunk, akkor ismerjük az implementáció részleteit, hiszen a mi forráskódunk szerepel a függvényben. Viszont ha a függvény bizonyítottan helyesen működik, akkor gyakorlatilag el is felejthetjük hogy milyen algoritmus van benne, elég csak arra emlékezni, hogy mi volt a neve a függvénynek, és hogy hogyan-mire használható.

A függvények alkalmazásával a procedurális paradigma szerinti programozást valósíthatjuk meg.

Az első függvényünk

Most hogy megteremtettem az ideológiát a függvények használatához, ideje megírni az első függvényünket. Elsőre még nehéz lehet meglátni, pláne forráskód nélkül, hogy vajon mi lesz majd az amit függvénybe akarnánk helyezni, de idővel annyira rááll az ember agya az alkalmazásukra, hogy gyanússá válik önmaga számára a saját munkája ha kevés a függvény a programban.

Kezdjük egy egyszerűvel: írjunk egy olyan függvényt ami eldönti egy számról hogy az páros-e vagy páratlan.

def is_even(number):
    return number % 2 == 0

Írjuk ezt be egy Python fájlba. Ha futtatjuk akkor a konzolban semmi se jelenik meg, azaz már megint ott vagyunk, ahol a tutorial legelső részében. De semmi gond, mindjárt minden világos lesz.

A fenti két sorban rengeteg új információ van. Vegyük az első sort:

  1. A def utasítással kezdjük meg a függvény definiálását.
  2. Szóközzel elválasztva következik a függvény neve, amire ugyan azok a szabályok érvényesek mint a változók neveire. Figyeljünk oda rá, hogy ne legyen átfedés a változók és függvények nevei között, ugyanis ezek felülírják egymást (a bejegyzés végén meglátjuk hogy miért).
  3. A függvény neve után zárójel következik, amiben megkezdjük a paraméterek felsorolását. Amikor majd a függvényt meghívjuk, és argumentumokat adunk át neki, akkor az argumentumokat az itt meghatározott sorrendben és a felsorolt neveknek megfelelő változókban érhetjük majd el.
  4. A függvény fejét kettősponttal zárjuk. Innen tudjuk, hogy blokk következik, ami a függvény törzse lesz.

A második sor már a törzs része, azaz ez a sor akkor fut majd le, ha meghívjuk a függvényt. Mint ahogy minden blokk, úgy ez is állhatna több sorból, de most egy egyszerű funkciót valósítottunk csak meg.

Ha az algoritmus befejezte a dolgát a függvényben, akkor a return utasítással juttathatunk vissza valamilyen értéket oda, ahol a függvényt meghívták. Ez lesz a függvény visszatérési értéke. A return csak függvényben szerepelhet, de ott akár több is lehet belőle, vagy el is hagyhatjuk teljesen. Természetesen először a jobb oldalán álló kifejezés értékelődik ki, és ha megszületett az eredmény, akkor történik meg maga a visszatérés.

Ha nem térünk vissza expliciten egy értékkel, akkor a Python impliciten visszatér helyettünk egy None-al.

Visszatérve tehát a forráskódhoz: a 2. sorban a number % 2 == 0 kifejezés eredményével tér vissza a függvény. Zavaró lehet hogy bár hivatkozunk a number változóra, az sosem kapott értéket. Ez két igen fontos okból sem probléma:

  1. Amikor definiáljuk a függvényt, akkor a függvény törzse nem fut le. Csupán szintaktikai ellenőrzés történik, azaz hogy helyes Python forráskód van-e a függvényben. Ha ez rendben megtörtént, akkor létrejön a függvény maga.
  2. A number változó nevét felsoroltuk a paraméterek között, tehát kijelentettük, hogy ha majd meghívjuk a függvényt, akkor a numer változónak lesz értéke. Éppen ezért ez a kifejezés gond nélkül ki tud majd értékelődni.

Most hogy ezt megtárgyaltuk, lássuk hogy működik-e a kódunk. Írjunk egy programot, ami a fenti függvény segítségével kiírja 0 és 10 között a páros számokat:

def is_even(number):
    return number % 2 == 0

for i in range(11):
    if is_even(i):
        print('Páros:',  i)

A 7. sorban látható az is_even() függvény meghívása, ami pont ugyan úgy történik mint bármelyik korábban bemutatott beépített függvény esetében. Abban a pillanatban hogy erre a sorra kerül a program futása, a Python összegyűjti a függvénynek adott argumentumok értékeit (ami most az i változó), és a program futása a 2. sorra ugrik. Itt a függvénynek adott argumentumokat az átadás sorrendjének megfelelően, a paramétersorban definiált nevekkel lehet elérni, azaz az i változó értékét a függvényen belül a numbers képviseli. A return visszatér az eredménnyel, ami ebben az esetben egy logikai érték lesz. A visszatéréssel együtt visszaugrik a futás a 7. sorra, ahol a hívás történt. Így már kiértékelődhet a feltétel, és értelem szerűen az i változó értékétől függően vagy kiírjuk hogy Páros, vagy nem jelenik meg semmi.

A függvény definiálása és a ciklus amiben a függvényt használjuk szépen elfér egymás mellett: a Python szép sorjában hajtja végre a program utasításait, így először a végrehajtja a függvény definiálását, majd halad tovább a for ciklus futtatására.

Változók láthatósága

A függvények saját kis elszigetelt környezetben futnak, ami a függvény fejénél rögtön el is kezdődik: a def is_even(number): esetén a number paraméter csakis a függvényben fog érvényben lenni. A függvényen belül tetszőlegesen deklarálhatunk új változókat, amik egészen addig élnek és használhatóak lesznek ameddig a függvény fut. Viszont ha a függvény visszatér, akkor visszatérési érték kivételével minden változó amit a függvényen belül használtunk meg fog szűnni, nem lehet őket többé elérni. Tehát ezek a változók nem láthatóak a függvényen kívül.

A függvényen belül viszont nem csak a paramétersorban definiált neveket, hanem a függvényen kívül deklarált minden más változót és értéket is látjuk (beleértve a függvényt saját magát is). Tehát a függvényen kívüli változók láthatóak a függvényen belül. Ez a tulajdonság garantálja hogy a függvény meghívhasson más függvényeket, elérhessen konstansokat, stb.

Csomagoljuk össze a fenti programot úgy, hogy legyen egy függvényünk, ami kap egy értéket, és visszatér egy olyan tömbbel amiben az összes az értéknél kisebb, nullánál nagyobb páros szám benne van:

def is_even(number):
    return number % 2 == 0

def even_numbers(limit):
    numbers = []
    for i in range(limit):
        if is_even(i):
            numbers.append(i)
    return numbers

print(even_numbers(11))

Ebben a példában jól látható, hogy az even_numbers() függvényben gond nélkül meghívhatjuk a kijjebb definiált is_even()-t.

Névterek

Azokat a területeket, ahol valamilyen változók elérhetőek (látszódnak), névtereknek (namespace) nevezik. A legkülső, legnagyobb névtér a globális névtér, ami gyakorlatilag a .py fájlnak felel meg. A globális névtér részei a beépített függvények, és persze minden amit magunk definiáltunk.

A függvények a meghívásukkor egy lokális névteret kapnak, amiben szabadon garázdálkodhatnak, így nem okoz gondot, ha két különböző függvényben azonos változóneveket használunk. A lokális névterek mindig öröklik a náluk nagyobb névterek értékeit is. Az öröklés miatt, amikor a változóra hivatkozunk, akkor a azt a Python először a lokális névtérben keresi, és onnan halad kifelé egészen a globálisig. Ezért oda kell figyelnünk: ha ki akarunk olvasni egy globális értéket, akkor ne hozzunk létre azonos nevű lokális változót, mert a lokálissal elfedhetjük a globálist.

Felmerülhet a kérdés viszont: ha a lokális névtérben látjuk a globális változókat, akkor lehetséges-e a módosításuk onnan?

Mik valójából a változók?

Álljunk meg egy pillanatra, mert ez fontos. Az egyszerűség kedvéért egészen eddig a bejegyzésig egyszerűen változóként hivatkoztam minden olyan adatra amit a számítógép memóriájában tárolunk el.

A valóságban, amikor változót deklarálunk, akkor a Python lefoglalja a tároláshoz szükséges memóriát és az épp érvényben levő névtérben létrehoz egy nevet ami erre a memóriaterületre mutat majd.

Ha kipróbáljuk a következő programot:

number + 1

Akkor a NameError: name 'number' is not defined hibaüzenetet kapjuk. Ez az üzenet arról árulkodik, hogy a number név nincs definiálva az érvényes névtérben.

Változtat-e ez az egész valamit azon amit idáig tudtunk? Nem. Mostantól a változó szó helyett a nevet fogom használni? Nem. Akkor ez most mire volt jó?

Nevek és névterek kapcsolata

Két nagyon fontos szabály érvényes a nevekre:
1. Lokális névtérből szabadon olvashatóak a globális nevei. Ezt láttuk már, így tudja elérni az egyik függvény a másikat
2. Lokális névtérből a globális nevek nem módosíthatóak alapértelmezés szerint. Azaz nem lehet olyat csinálni, hogy a lokális névtérből átállítunk egy globális nevet úgy, hogy az egy eltérő értékre mutasson a memóriában.

Nem-alapértelmezés szerint mégis megoldható a módosítás, de ezzel most nem foglalkozunk mert így se leszünk korlátozottak programozás közben.

Mit jelent mindez a gyakorlatban?

Vegyünk egy egyszerű problémát: írjunk egy olyan függvényt ami számolja hogy hányszor volt meghívva. Ahhoz hogy ez sikerülhessen el kell érni, hogy legyen egy változónk valahol a függvényen kívül, ami akkor is megőrzi az értékét, amikor a függvény befejezte a futását. Mi sem egyszerűbb ennél:

call_counter = 0

def a_function():
    call_counter += 1

a_function()

Ha lefuttatjuk ezt a kis kódot, akkor a UnboundLocalError: local variable 'call_counter' referenced before assignment hibaüzenetet kapjuk.

Ez azért történik, mert bár a call_counter változó látható volt az a_function()-ből, de amikor írni akartuk az értékét, akkor a Python a lokális névtérben létrehozta a tároláshoz szükséges nevet. Ez a név megegyezik azzal, ami a a globálisban is szerepel, így az elfedésre került. Ez még nem is lenne baj, viszont a létrejött névhez nem tartozik érték (nem került inicializálásra), így nem lehetséges az értékének növelése, és ez eredményezi a hibát.

Itt van viszont egy másik, érdekes megoldás:

call_counter = [0]

def a_function():
    call_counter[0] += 1

a_function()
print(call_counter)

Itt a call_counter egy 1 elemű tömb. Az első elem értékenek 0-át állítottunk. Ebben nincs semmi különös, tetszőleges mennyiségű-minőségű elem állhatna a tömbben.

Az a_function() törzsében a a call_counter tömb 0. indexű elemének értékét növeljük, ami minden gond nélkül lehetséges, hisz nem a call_counter-t próbáltuk meg lecserélni, ami ugye nem lehetséges, hanem az egyik abban található elemet, és ez teljesen szabályos lépés.

Ez lenne a megoldás a problémára? Így tudunk “kimenteni” értékeket a függvényből? Egyáltalán nem. Ezzel a példával akarom bevezetni az eddig nem ismertetett, utolsó fontos tulajdonságát a Python típusainak

Változtatható, változtathatatlan értékek

A Python nyelvben a típusokat a szerint is megkülönböztethetjük hogy változtatható (mutable) vagy változtathatatlan (immutable) értékek tartoznak hozzájuk.

A változtathatatlan értékeket általában primitívek, és ha egyszer definiálva lettek, akkor nem lehet módosítani őket. A módosítást csak úgy lehet elérni, hogy felüldefiniáljuk a meglevő változót. Az int, float és str értékek ilyenek.

Bár lehet eddig nem tűnt fel, az említett felüldefiniálást folyamatosan végezzük amikor leírunk egy olyan sort például hogy counter += 1.

Ez a tulajdonság az utolsó kis részlet abban a történetben, ami miatt nem volt sikeres a call_counter növelése akkor, amikor az int típusú volt. Az érték változtathatatlansága miatt a Python kénytelen lenne a megnövekedett érték memóriaterületére átállítani a globális névtérben szereplő nevet, de ahogy azt már sokszor mondtam, ez nem kivitelezhető.

A változtatható értékek ezzel szemben nem primitívek, hanem objektumok. A változtathatóság alatt azt értjük, hogy lehetséges érték valamilyen tulajdonsága, például a felsorolásának a módosítása. Változtatható értékek a list és a dict. Így volt lehetséges a call_counter 0. indexű elemének a módosítása, hiszen azzal a lista egy elemét módosítottuk, nem magát a listát.

Az értékadás típusa

Sokat beszéltünk a névterekről, de most újra fókuszáljunk csak a függvényekre.

Amikor értéket adunk át egy függvénynek, akkor a Python a paramétersorban felsorolt neveket az átadott értékek memóriaterületére állítja. Ez azt jelenti, hogy a függvényen belül pontosan ugyan azokat az értékeket írjuk és olvassuk mint amiket a függvény argumentumul kapott. Ezt referencia szerinti értékátadásnak hívják (pass by reference).

Másik módszer az érték szerinti átadás (pass by value), amikor a függvények argumentum értékek másolatait kapják meg. Pythonban ez nem létezik.

Vegyük a következő kódot:

call_counter = 0

def a_function(counts):
    counts += 1

a_function(call_counter)
print(call_counter)

A 6. sorban argumentumul adtuk a call_counter változót a függvénynek, amit a függvényen belül a counts névvel érhetjük majd el. A 4. sorban, a counts += 1-el növeljük az értéket.

Ha futtatjuk a programot, akkor a konzolban várhatóan az 1-es szám jelenik meg, hisz a függvényben ugyan abba a memória területre írtunk mint ahova a counts mutat. Sajnos azonban a Python máshogy működik, ezért jelent meg a 0 a konzolban.

Mivel a counts paraméter típusa egy int, ami immutable, ezért az érték növelésével a counts változót mindenképp új memóriaterületre fog mutatni. A függvényen belül azonban sohasem tudjuk a paraméterül kapott referenciát állítani, ugyan úgy, mint ahogy a lokális névtérből nem módosíthatunk egy globális nevet.

Mutable típusoknál természetesen nincs ilyen megkötés. Ez egyszerre jó is, meg nem is: jó mert írhatunk függvényeket, amik nem térnek vissza semmivel, hanem közvetlenül módosítják a nekik adott változtatható értéket. Viszont ha azt akarjuk hogy bár a függvényen belül módosult legyen az érték, de azon kívül ne változzon semmi, akkor kézzel másolatot kell készíteni a paraméterről. Ha erről elfeledkezünk, akkor azon kapjuk magunkat, hogy az argumentumul adott érték akaratunktól kívül is változik, ami egyértelműen rossz.

Írjunk egy olyan függvényt, ami kap egy tömböt és minden értéket megkétszerez benne:

def double(nums):
    for i in range(len(nums)):
        nums[i] *= 2

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

A double() függvény várja a tömböt, ami a nums nevet kapja majd. A függvény törzsében indítunk egy ciklust, ami végig iterálja a tömb indexeit, így a ciklus törzsében minden értéken elvégezhejük a 2-vel való szorzást.

A függvényen kívül, az 5. sorban deklaráltuk a numbers tömböt. Miután átadjuk ezt a double() függvénynek, a tömb kiírásakor látszódik majd, hogy a numbers elemei már többé nem az [1, 2, 3], hanem a [2, 4, 6].

Opcionális és tetszőleges számú paraméterek

Ha megfigyeljük, akkor a print()-nek akármennyi argumentumot adunk, azokat mind ki fogja írni. A sorted() függvény esetében ha fordított rendezést akarunk elérni akkor írhatjuk hogy sorted(numbers, reverse=True), de a reverse=True nélkül is működni fog a program.

Ez utóbbi lehetőséget hívjuk opcionális paraméternek. Felsorolhatunk a függvény fejében úgy paramétert, hogy annak rögtön egy kezdő értéket is adunk. Így a paraméter kitöltése opcionálissá válik.

A Pythonos elnevezése a technikának a keyword argument (kwarg). Ez egyszerre jelenti azt, hogy a paraméter opcionális, és hogy úgy adható neki érték, hogy a híváskor kiírjuk hogy melyik paraméternek mi lesz az értéke. Az opcionális paraméterek mindig a függvény fejének végén kerülnek felsorolásra, a közönséges paraméterek után.

Írjunk egy egyszerű kis függvény ami 2-vel megszorozza a neki adott számot, vagy opcionálisan saját szorzót is adhatunk neki:

def multiply(number, multiplier=2):
    return number * multiplier

print(multiply(3))
print(multiply(3, multiplier=10))

A függvény fejét kell nézni, a multiplier=2-vel definiáltuk az opcionális paraméter értékét. Ilyen egyszerű az egész.

Az első hívás során, a 4. sorban csak az egyetlen kötelező argumentumot adtuk meg, így a 2-vel való szorzás fog végrehajtódni.
Az 5. sorban látható hogy hogyan definiálhatjuk az opcionális paraméter értékét: hasonlóan a függvény fejéhez, ki kell írni hogy melyik opcionális paraméternek milyen értéket adunk.

Mivel az opcionális paramétereket a nevük alapján azonosítja a Python ezért ezeknek a sorrendje nem kötött.
Az opcionális paraméterek felvehetik változó értékét is, abból a névtérből ahol a függvény definiálva lett.

A print()-re visszakanyarodva. Írhatunk olyan függvényt ami tetszőleges számú közönséges vagy opcionális paramétert képes fogadni. Ennek kipróbálására írjunk egy programot ami az összes számot összeadja amit argumentumként kap:

def add_all(*numbers):
    result = 0
    for number in numbers:
        result += number
    return result

print(add_all(1, 2, 3, 4, 5))

A függvény fejében a *-al megjelölt változó (esetünkben a *numbers) minden argumentumot összegyűjt, és azokat egyetlen felsorolt típusban, egy úgynevezett tuple típusú értékben érhetjük el. Így egyszerűen a for ciklussal összeadhatjuk az argumentumokat. A 7. sorban látható, hogy az add_all() függvény akárhány argumentum fogadására képes.

A tuple-ről eddig nem volt szó. Ez egy a list-hez nagyon hasonló adattípus, azzal a különbséggel hogy a benne felsorolt értékek nem változtathatóak, azaz immutable.
Vegyük észre azt is, hogy csak a függvény fejében szerepel a *, a törzsben már nem!

Tetszőleges számú opcionális paraméter fogadásához a ** jelzőt kell használni:

def list_kwargs(**kwargs):
    for key, value in kwargs.items():
        print('{0} = {1}'.format(key, value))

list_kwargs(name='Slapec', year=2016)

Ez egy nem túl okos függvény, de példának most megteszi. A függvény paramétersorában a ** jelzővel állátott kwargs nevű paraméterben érhető majd el minden opcionális paraméter amivel a függvényt meghívjuk. Ez a kwargs változó egy dict-et tartalmaz majd. A függvény belsejében egyszerűen csak végig iteráljuk ennek a paraméternek az értékét, és kiírjuk a kulcs-érték párokat egymás alá.

A Python fejlesztők gyakran használják a *args és **kwargs elnevezést az opcionális paraméterek jelzésére, de ahogy látható volt a példában, ezek a szavak nem bírnak megkülönböztetett jelentéssel.

Az * és ** jelzők szabadon kombinálhatóak a többi közönséges vagy opcionális paraméterrel:

def add_and_multiply(base, *numbers, multiplier=2):
    result = base
    for number in numbers:
        result += number
    return result * multiplier

print(add_and_multiply(0, 10, 20, 30, multiplier=10))

A paramétersorban csak egy-egy olyan paraméter lehet, amik * vagy ** jelzőt kapnak!

A fenti esetben az add_and_multiply() függvény a következő argumentumokat várja:

  1. Kötelező első argumentumnak egy alap számot adni, amihez a többi értéket majd hozzá adjuk. Ez a base nevet kapja.
  2. Ez után tetszőleges számú közönséges argumentum következik amik a `numbers*
  3. Opcionális paraméterként megadhatunk egy szorzót, amivel az összeget megszorozza a függvény. Ez a multiplier kwarg lesz.

A * jelző minden közönséges argumentumot begyűjt, így ez után már csak és kizárólag opcionális paraméterek következhetnek. A ** az összes opcionális argumentumot begyűjti, így ez után nem állhat semmilyen paraméter.

A legújabb Python verziókban viszont állhat közönséges paraméter a * után is. Ez a paraméter, bár közönségesnek tűnik, mivel a * után következik ezért már keyword argument-nek számít, de mivel nincs értéke ezért kötelező a kitöltése. Ez azt jelenti, hogy a függvény hívása során az argumentum megadása kötelező, és kötelező a paraméter nevét is kiírni, mint bármely más kwarg esetén.

Az egyszerűség kedvéért az előző példát írjuk át:

def add_and_multiply(base, *numbers, multiplier):
    result = base
    for number in numbers:
        result += number
    return result * multiplier

print(add_and_multiply(0, 10, 20, 30, multiplier=2))

A függvény fejében a *numbers után következik a multiplier paraméter, aminek nincs kezdeti értéke. A függvény hívás megmaradt, viszont ha nem írjuk ki expliciten a multiplier=2, akkor hibát kapunk a programtól.

Argumentumok listaként, lista argumentumokként

Ahogy a paramétersorban a * és a ** az argumentumok felsorolását tuple és dict típusúvá alakította, úgy a függvény hívásakor (és csak ott!) az argumentumlistában ezek a csillagok a felsorolásokat vagy dict-et valódi argumentumokká alakítják.

Ez azt jelenti, hogy meghívhatunk úgy függvényt, hogy az egyetlen list-et kap paraméterül, és a list minden értéke a függvény fejében egy-egy paraméterbe kikerül, hasonlóan mint az unpacking esetén.

Írjunk egy egyszerű kis függvényt ami kiszámolja egy háromszög kerületét:

def perimeter(a, b, c):
    return a + b + c

sides = [3, 4, 5]
answer = perimeter(*sides)
print(answer)

A perimeter() függvénynek, ami kiszámolja a háromszög kerületét, három kötelező argumentumot vár. Ennyit elég is tudni a működéséről.

Az átadni kívánt paramétereket a 4. sorban egy közönséges list-ben soroltuk fel. Ez után az 5. sorban látható, hogy a *sides kifejezéssel a három elemű tömbből a függvény számára három argumentumot készítettünk.

A függvény objektum

Az tiszta sor, hogy a függvényt úgy hívjuk meg hogy zárójelet teszünk a végére, például így: print(). De mi van akkor, ha csak annyit írunk le hogy print?

print(print)

A Pythonos filozófia szerint a print név (de minden saját függvényünk is) a memóriában a függvény kódjára (függvény objektumra) mutat. Ha elhagyjuk a zárójeleket, akkor a kód sosem kerül futásra, és hasonló műveleteket végezhetünk a függvényre mutató névvel mint bármely más változóval. Például átadhatjuk paraméterül, sőt, vissza is térhetünk vele.

A zárójelek kiadásával a Python feltételezi, hogy a memória ahova a név mutat futtatható objektumot tartalmaz. Értelem szerűen bármely érték végére írhatnánk zárójeleket, de az int, str és a többi jól ismert típusnak megfelelő érték nem képes ilyen viselkedésre. A függvény neve viszont igen.

Azokat az értékeket amiket futtatni lehet callable-nek (meghívható objektum) nevezik.

Tehát ezek szerint egy függvény várhat egy függvény kódjára, amit aztán majd saját maga fog lefuttatni. Hasonlóan, a függvény dinamikusan összeállíthat függvényt a saját törzsében, és visszatérhet azzal az értékkel, ami erre a dinamikus függvényre mutat.

Ez utóbbi esettel egyelőre nem foglalkozunk, viszont az, hogy egy függvénynek függvényt kell paraméterül adni elég gyakori a Python nyelvben.

Írjunk egy olyan programot, ami két elemű tömbök felsorolását sorba rendezi a kis tömbök második értéke szerint. Ahhoz hogy ez lehetséges legyen meg kéne mondani a sorted() függvénynek, hogy mely elemek szerint tegye sorrendbe a listát. Ehhez a sorted()-nek egy másik függvényt, úgynevezett key functiont kell átadni. Ide kerülhet az a logika, ami visszatér majd az elemmel ami szerint megtörténik a sorba rendezés.

def key_function(elem):
    return elem[1]

ages = [('Alice', 24), ('Bob', 20), ('Carol', 22)]

print(sorted(ages, key=key_function))

A key_function() függvény szigorúan egy argumentumot fogadhat, mert tudjuk, hogy amikor a sorted() meghívja majd a függvényünket akkor pontosan ennyi argumentumot ad majd neki. Azt is tudjuk, hogy ez az egy érték mindig az épp soron következő kis tömb lesz majd a felsorolásból. A kis tömböket a 2. elemük szerint akarjuk sorba rendezni, ezért a függvényből az elem[1]-el térünk vissza.

A 6. sorban jön az izgalmas rész végre. A sorted() függvény opcionális key argumentumában kell átadni a függvényt. Nagyon figyeljetek rá, hogy nincs zárójel a key_function végén.

Ezzek a kifejezéssel lesz kerek a történet. Lássuk sorjában hogy hogyan fut le a program.

  1. A sorted() veszi az első elemet az ages tömbből, ami a ['Alice', 24]. Ezt a tömböt argumentumként használva meghívja a key_function-t.
  2. A key_function megkapta a ['Alice', 24]-t elem névvel. A függvényből visszatérünk az elem[1]-el, ami a 24 lesz.

A fenti két lépés szép sorjában ismétlődik az ages összes elemére. A művelet vége a rendezett tömb lesz.

Lambdák

Levezetésnek néhány sor a lambdákról. A lambda kifejezéssel a Pythonban rövid kis eldobható függvényeket gyárthatunk. Ezek a függvények kiválóak rendezésekhez.

Írjuk át a fenti kódot így:

ages = [['Alice', 24], ['Bob', 20], ['Carol', 22]]

print(sorted(ages, key=lambda elem: elem[1]))

A sorted() key argumentuma mellett látható a lambda kifejezés. Ez a Python nyelvben egy utasítás, ami egy függvény előállítására alkalmazható. A lambda szó után következik a függvény paramétersora. Erre a sorra ugyan azok a szabályok érvényesek mint a közönséges függvényekre. A paramétersort itt is a : zárja, de ez után -szokatlanul- nem kell blokkot nyitni: a lambda törzse és a feje egy sorban követik egymást. A törzsbe egyetlen kifejezés kerülhet, ami a lambda függvény visszatérési értéke lesz majd.

Látható hogy a lambda igen szigorú szabályok mellett működik. Amiatt hogy egyetlen sorból áll és egy kifejezést tartalmazhat, nem érdemes komplex logikát bele írni. Olyankor használjuk közönséges függvényeket.

Zárás

Most rengeteget beszéltünk a függvényekről, de ezzel a nagyján túl is estünk, és majd (nem olyan túl sokára) a metódusok bevezetésénél nagyon örülni fogunk, mert minden igaz lesz rájuk amik a függvényekre is.

A következő bejegyzés biztosan nem lesz ilyen baromi hosszú. Megnézzük hogy hogyan reagálhatunk hibákra a programunkból, és hogy hogyan bonthatjuk fájlokra (modulokra) a szoftvert.

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

-slp

2016. március 18., péntek

06 - str, list, dict: az érdekes részek

Az előző részben eljutottunk a vezérlési szerkezetekig, így már gond nélkül írhatunk gondolkodó programot. A nagy rohanásban viszont (szándékosan) elsiklottam jó néhány részlet felett. Ezeket vesszük most át. Akkor GO.

Értékadás rövidebben

Többször láttunk már olyan forráskódot, ahol valami ehhez hasonló szerepelt:

counter = 0
counter = counter + 1

A második sor az érdekes: ha az értékadás operátor bal- és jobb oldalán ugyan az a változó szerepel, akkor azt így rövidíthetjük:

counter = 0
counter += 1

Mindegyik operátornak létezik ilyen rövidített alakja.

Más nyelvekben az érték 1-el való növelésére/csökkentésére használható a ++ vagy -- operátor. Pythonban nincs ilyen.

Ez angolul ezt compound assignment-nek (Python dokumentációban augmented assignment-nek) nevezik. Magyarul sajnos nem találtam rá kifejezést.

Felsorolt típusok

Az str és list felsorolt (sequence) típusai a Python nyelvnek. Amikor egy str típusú értéket írunk le, akkor a Python szemszögéből karakterek felsorolástát végezzük (ezért is hívják néha a stringet karakterláncnak). list esetén expliciten, vesszővel elválasztva írjuk fel tömb elemeit. A felsorolás egyik jelentős tulajdonsága, hogy sorfolytonos, tehát a felsorolás minden elemének eggyel növekvő indexe van. Az első elem indexe a 0, és nem maradnak ki indexek a sorból.

Azt már tudjuk, hogy a felsorolás egy elemét az index operátorral ([ ]) tudjuk elérni, de még egy sor tulajdonság igaz rájuk. Vegyünk párat ezek közül.

Slicing

Az index operátorban nem csak egy értéket, hanem akár egy intervallumot is megadhatunk, így megkapjuk a felsorolás egy szeletét, ez a slicing.

Az intervallum balról zárt, jobbról nyitott.
A slicing során nem törlődnek a kiszeletelt értékek az eredeti felsorolásból, hanem másolatot kapunk róluk.

Itt egy példa hozzá:

a_string = 'Python'
some_chars = a_string[2:4]
print(some_chars)

A 2. sorban látható a varázslat: a [ ] operátor között az intervallum elejét és végét a : karakterrel választjuk el. Ezzel a kifejezéssel a 'Python' string 2-4 indexei közötti szeletet kapjuk meg (a 2. indexű elem (ez a t karakter) már benne lesz a szeletben, de a 4. indexű (az o karakter) már nem.). Így tehát a th értéket veszi fel a some_chars változó. Ez is jelenik meg a képernyőn ha futtatjuk a programot.

A slice legfeljebb 3 értéket tartalmazhat. Nézzük szép sorjában:

  • [start:stop] - Ez volt látható a példában is: a start-tól a stop-ig kapunk értékeket.
  • [start:stop:step] - Opcionálisan a 3. értékben (step) definiálhatjuk, hogy a szeletből csak minden valahányadik értéket akarjuk megkapni.

Léteznek rövidebb formulák, ezek nagyon gyakoriak:

  • [start:] - Ha a kezdő start értéktől a felsorolás végéig kérjük a szeletet, akkor a stop elhagyható. Így szoktak a felsorolás elejéről “levágni” elemeket.
  • [:end] - Hasonlóan, ha a felsorolás elejétől az end értékig kérjük a szeletet, akkor a start elhagyható. Így szoktak a felsorolás végéről “levágni” elemeket.
  • [:] - Ha se start, se stop nincs, akkor értelem szerűen a felsorolás másolatát kapjuk meg, minden elemmel.

A fenti esetek kombinálhatóak a 3. step értékkel is. A lényeg hogy megfelelő számú : maradjon a kifejezésben. Nézzünk egy olyan példát, ahol egy list minden páros indexű elemét akarjuk kigyűjteni:

numbers = [0, 1, 2, 3, 4, 5]
even = numbers[::2]
print(even)

Vagy ahol a páratlan indexűeket:

numbers = [0, 1, 2, 3, 4, 5]
odd = numbers[1::2]
print(odd)

Így az 1. indexű elemet, majd az (1+2) indexűt kapjuk, és így tovább …

Vágjuk le az első és utolsó elemet:

numbers = [0, 1, 2, 3, 4, 5]
print(numbers[1:len(numbers)-1])

Ahhoz hogy ez sikerüljön, a len() függvénnyel le kellett kérni a numbers tömb hosszát. Ha az intervallum eddig tartana, akkor minden értéket megkapnánk a felsorolás végéig, ezért kivontunk 1-et belőle, így az utolsó elemet ki tudtuk hagyni a szeletből. Gondolkodás szempontjából nagyszerű ha erre a műveletre magunktól rájövünk, de Pythonban létezik egy ennél sokkal elegánsabb megoldás is.

Fontos: üres objektumot kapunk akkor, ha olyan intervallumot állapítunk meg, amibe nem esnek elemek a felsorolásból (pl.: az intervallum kezdő indexe nagyobb, mint a felsorolás utolsó indexe).

Negatív indexelés

A felsorolás bal oldalán 0-tól induló, szigorúan monoton 1-el növekvő indexet már sokat láttuk. E mellett használhatunk negatív indexet is, ami a felsorolás jobb oldalán -1-től indul, és szigorúan monoton 1-el csökken. Ez a kis táblázat segít megérteni ezt a párhuzamos dimenziót:

P y t h o n
0 1 2 3 4 5
-6 -5 -4 -3 -2 -1

Az 1. sorban a természetes index látható, amit már jól ismerünk. A 2. sorban az ezzel párhuzamos, negatív indexek olvashatóak. Negatív indexet bármely felsorolt típusnál használhatunk, és slice-ba is rakható.

Akkor vágjuk le a Python szó első és utolsó karakterét:

print('Python'[1:-1])

Olyan kényelmes, már-már a a hihetőség határát súrolja.

Unpacking

Az előző bejegyzésben, a for ciklus kapcsán már találkoztunk az unpacking-el. Ez egy nagyon hasznos művelet a Pythonban, így ugyanis egyetlen utasítással ki tudjuk tenni egy tömb értékeit egy kupac változóba.

Gyakorlatilag, amikor ezt írnánk:

values = [1, 2, 3]
first = values[0]
second = values[1]
third = values[2]

Akkor helyette írhatjuk ezt:

values = [1, 2, 3]
first, second, third = values

Hát ez nem baromi jó?

Természetesen ez ilyen formában csak akkor működik, ha a tömb minden eleméhez tudunk egy változó nevet társítani. Tehát fontos: 3 elemű tömböt 3 változóba kell unpack-elni

Klasszikus probléma amikor meg akarjuk cserélni két változó értékét egymással. Klasszikusan ez csak egy segédváltozó bevezetésével lehetséges:

x = 1
y = 2
print(x, y)

temp = y
y = x
x = temp
print(x, y)

A 5. sorban a temp változóban eltároljuk az y értékét, ugyanis nemsokára felülírjuk ezt a változót, így elveszne az értéke. Az 6. sorban meg is történik a felülírás, itt cserélődik meg az y az x-szel. A 7. sorban az x-nek beállítjuk a temp értékét, ami régi y értéke volt. Így megcseréltünk két változót.

Vagy csinálhatjuk így is:

x = 1
y = 2
print(x, y)

x, y = y, x
print(x, y)

Ahol az 5. sorban az y, x kifejezést kicsomagoljuk az x, y változókba, így meg is cserélődött a két változó értéke

Elem létezésének vizsgálata

Ha arra vagyunk kíváncsiak, hogy egy felsorolás tartalmaz-e egy konkrét értéket, akkor az in operátort kell használnunk.

Írjunk egy programot, ami megmondja, hogy egy konkrét szó szerepel-e egy szövegben:

text = 'Itt Slapec beszél'
slapec_is_speaking = 'Slapec' in text
print(slapec_is_speaking)

A 2. sor az érdekes: a kifejezés True-ra értékelődik ki, mivel a ‘Slapec’ string szerepelt a name változóban, így ez is jelenik meg a konzolban.

Szerintem ez egy nagyon intuitív eszköze a nyelvnek, szinte teljes angol mondatokkal programozunk.

Elem létezésének kizárása

Hasonlóan intuitívan zárhatjuk ki,hogy egy elem létezik-e egy felsorolásban, a not in operátorral.

Írjunk egy programot ami kiírja, hogy az Alice név hiányzik-e a tömbből:

names = ['Bob', 'Carol']
alice_is_missing = 'Alice' not in names
print(alice_is_missing)

Azt hiszem itt már nincs is mit magyarázni.

Természetesen a kizárást úgy is ellenőrizhettük volna, hogy azt a kifejezést írjuk meg, ami azt vizsgálja Alice benne van-e a tömbben, majd a not operátorral ezt tagadjuk, így az ellentétes eseményre kapjuk meg a választ, azaz hogy Alice nincs benne a tömbben. Magyarul: alice_is_missing = not ('Alice' in names), de ilyet nem ír senki.

Elem törlése

A del operátorral törölhető egy index a tömbből.

numbers = [0, 1, 2]
del numbers[0]

print(numbers)

Így a konzolban csak a [1, 2] látszódik majd.

Cikluson belül óvatosan töröljünk az iterált felsorolásból. Iteráció közben változtatni az objektumon nem várt eseményeket okozhat (átugorhatunk elemeket).

Műveletek felsorolásokon

Létezik egy nagyobb marok beépített függvény a Python nyelvben, amivel mindenféle műveletet végezhetünk el felsorolt adatokon. Most ezek következnek, a teljesség igénye nélkül.

A felsorolás hossza

A len() megmondja a neki adott objektum elemeinek számát (hosszát).

long_string = 'Egy hosszú szöveg, sok karakterrel'
long_string_length = len(long_string)
print('Az "', long_string, '" mondat', long_string_length, 'karakterből áll.')

names = ['Alice', 'Bob']
print('Összesen', len(names), 'név szerepel a listában.')

Így a konzolban a Az ” Egy hosszú szöveg, sok karakterrel ” mondat 34 karakterből áll majd az Összesen 2 név szerepel a listában feliratok jelennek meg. Ez azért elég hasznos.

Legkisebb, legnagyobb értékek

A min() és a max() függvények visszaadják a neki adott felsorolás legnagyobb/legkisebb értékeit. Tök ugyan úgy kell használni őket, csak hát más eredményt szolgálnak.

Írjunk egy programot ami megmondja hogy egy diák jegyei közül melyik volt a legjobb és a legrosszabb:

marks = [2, 3, 3, 5, 5, 4]
print('A legjobb jegyed ez volt:', max(marks))
print('A legrosszabb pedig ez:', min(marks))

Felsorolás értékeinek összeadása

A sum() függvénnyel összeadható egy felsorolás összes értéke. Értelem szerűen ez csak akkor lehetséges, ha minden elem numerikus.

Írjunk egy programot ami megmondja egy diák jegyeinek az átlagát:

marks = [2, 3, 3, 5, 5, 4]
average = sum(marks) / len(marks)
print('Az átlagod:', average)

Ehhez ugye össze kell adni minden jegyet s az eredményt el kell osztani a jegyek számával. Összeadni a sum()-al tudunk, a jegyek számát a len() megmondja, kész is a csoda szoftver.

Elemek sorba rendezése

A sorted() függvénnyel növekvő sorrendbe állíthatóak egy tömb elemei

numbers = [4, 5, 1, 3, 2]
numbers_in_order = sorted(numbers)
print(numbers_in_order)

Csökkenőbe rendezéshez a reverse=True argumentumot kell a függvénynek adni:

numbers = [4, 5, 1, 3, 2]
numbers_in_order = sorted(numbers, reverse=True)
print(numbers_in_order)

Ha összetettebb dolgokat, esetleg több szempont szerint szeretnénk sorba rendezni, akkor egy függvényt is átadhatunk a függvénynek, ami eldönti hogy két elem közül melyik a kisebb/nagyobb. Mivel egyelőre még nem tudjuk hogy hogy kell függvényt írni, ezért erre a lehetőségre majd a következő részben fogunk visszatérni.

Felsorolás megfordítása

A reversed() függvénnyel el tudjuk készíteni a felsorolás fordított másolatát. A fenti példák alapján ennek triviálisnak kéne lennie:

numbers = [5, 4, 3, 2, 1]
reversed_numbers = reversed(numbers)
print(reversed_numbers)

Viszont csak a(z egyelőre nem túl sokat mondó) <list_reverseiterator ... felirat fogad minket a konzolban. Ezt azért csinálja velünk a Python hogy memóriát spóroljon. Szerencsére ha a list() függvénynek átadjuk ezt az ismeretlen objektumot, akkor megkapjuk a tömböt:

numbers = [5, 4, 3, 2, 1]
reversed_numbers = list(reversed(numbers))
print(reversed_numbers)

Érdekességként megjegyezném, hogy slice-al is megfordítható a tömb, ha lépésszámnak -1-et állítunk.

numbers = [5, 4, 3, 2, 1]
reversed_numbers = numbers[::-1]
print(reversed_numbers)`

Természetesen az str-ről se feledkezzünk meg:

print('Hello World'[::-1])

A -1 nem kitüntetett érték a slice életében. Tetszőleges negatív számot használhatunk, akkor annyival haladunk visszafelé.

Metódusok

A metódusok (method) témát az előző bejegyzésben súroltuk már, amikor a dict-et kulcs-érték páronként iteráltuk a for ciklusban.

Egy kicsit még jegelném, hogy hogyan is működik ez a varázslat pontosan. Annyit elég tudni dióhéjban, hogy azok a változók amiket idáig megismertünk nem csak magát az adatot tárolják (str típusú a karaktereket, a list a különböző elemeket, vagy az int egy egész számot), hanem az adaton elvégezhető műveleteket is. Ez az egyik olyan tulajdonság, ami miatt minden érték objektum a Python nyelvben.

A metódus hívása nagyon hasonló a függvényéhez, viszont amíg a függvény önmagában szerepel (pl.: len()), és kézzel kell neki átadni az adatot, addig a metódusokat mindig valamilyen kifejezésen keresztül lehet elérni a . (dot operator) segítségével. Attól függ hogy milyen metódusokat érhetünk el, hogy milyen típussal értékelődött ki a kifejezés. Ezen kívül ugyan úgy adhatóak argumentumok egy metódusnak mint egy függvénynek.

A következő bejegyzésben sok példát láthattok erre, mivel rengeteg metódusa van a Python típusainak.

A list metódusai

Elem hozzáadása

A legfontosabb metódus az .append(). Ezzel lehet egy elemet a tömb végéhez hozzáadni.
Írjunk egy programot, amiben egy ciklus elhelyezi egy tömbbe a számokat 0-tól 10-ig:

numbers = []
for i in range(11):
    numbers.append(i)
print(numbers)

Az 1. sorban egy tök üres tömböt hoztunk létre. A 3. sorban látható az új szerkezet.
A numbers változó értéke list, innen tudjuk hogy rendelkezik .append() metódussal. Ez a metódus 1 argumentumot vár, azt az objektumot amit önmagának a végéhez hozzá kell adnia. Azaz a teljes kifejezés azt jelenti, hogy a numbers értékéhez hozzáadjuk (hozzá fűzzük) az i változó értékét. Az utolsó sor futásával látszódik is, hogy szépen feltöltődött a tömb.

Elem beszúrása

Az .insert() metódus tetszőleges indexre beszúr egy elemet. A beszúrt elem helyétől minden elem jobbra tolódik. Használhatunk negatív indexet is, és ha nagyobb indexet állítunk mint ahány elem van a tömbben akkor a tömb végére kerül az új elem.

numbers = [0, 1, 2, 3, 4]
numbers.insert(2, 5)
print(numbers)

A konzolban a [0, 1, 5, 2, 3, 4] sor jelenik meg. Látható hogy az 5 bekerült a 2. indexre, és minden elem jobbra eltolódott tőle.

Ahogy az .append()-el a tömb végére helyeztünk egy elemet, úgy a 0. indexre beszúrással a tömb elejéhez rakhatunk:

numbers = [0, 1, 2, 3, 4]
numbers.insert(0, 5)

print(numbers)

Elemek sorba rendezése, felsorolás megfordítása

Ahogy korábban látható volt, ezt a két funkciót a sorted() és reversed() függvények már megvalósították, azzal a különbséggel, hogy azok mindig az eredeti felsorolás másolatát eredményezték.

Ha másolat készítés nélkül, helyben (in-place) akarunk rendezni vagy megfordítani egy tömböt, akkor használjuk a .sort() vagy .reverse() metódusokat.

random_numbers = [4, 5, 1, 3, 2]
random_numbers.sort()
print(random_numbers)

descending_numbers = [5, 4, 3, 2, 1]
descending_numbers.reverse()
print(descending_numbers)

Mivel ezek a metódusok a tömböt helyben módosítják, ezért a metódus hívása nem eredményez visszatérési értéket.

Az str metódusai

Az str metódusai sohasem változtatják meg a szöveget, hanem a módosított stringgel térnek vissza. Ennek az okára rögtön a következő nagy fejezetben rá is térünk, de addig jöjjön néhány hasznos metódus:

Átalakítás csupa kis/nagybetűssé

A .lower() és .upper() metódusok visszaadják a string csupa kicsi- vagy nagybetűs változatát:

message = 'Hello World'

print(message.lower())
print(message.upper())

Akkor lehet hasznos ez a két metódus ha két string tartalmi egyezőségét akarjuk vizsgálni.

Bár idáig nem említettem, de a Python (ahogy a legtöbb más nyelv is) úgynevezett kis/nagybetű érzékeny (case sensitive) programozási nyelv. Ez azt jelenti, hogy a nyelv különbözőnek tekint két kifejezést akkor, ha a keverjük bennük a kicsi- és nagybetűket.

Így tehát ez a feltétel hamis:

print('Hello World' == 'hello world')

De például a print() helyett se írhatunk Print()-et, mert az már egy másik függvény lenne, a nagy kezdőbetű miatt, ami nem létezik.

Ha gondoskodunk róla hogy két string egyformán csupa kicsi vagy nagybetűs legyen (normalizáljuk a szöveget), akkor az egyezés vizsgálatába nem zavar be többé ez a különbség. Ezt hívják kis/nagybetű érzéketlenségnek (case insensitive).

Írjuk át ennek szellemében a kódot:

print('Hello World'.lower() == 'hello world'.lower())

Mint általában, a normalizálás akkor szükséges, ha a felhasználótól olvasunk be adatot, mert sosem tudják normálisan begépelni azt amire kérjük őket.

Szöveg valamilyen stringgel kezdődik vagy végződik

A startswith() és endswith() metódusok True-val térnek vissza, ha a string a függvénynek adott argumentummal kezdődik vagy végződik.

Írjunk egy programot ami ellenőrzi, hogy egy begépelt fájl kiterjesztése mp3-e:

path = input('Elérési útvonal: ')

if path.endswith('.mp3'):
    print('Ez egy mp3')

Egyébként slice-al is megoldható a probléma, ha a string utolsó 4 karakterét hasonlítjuk, így:

path = input('Elérési útvonal: ')

if path[-4:] == '.mp3':
    print('Ez egy mp3')

Szöveg szétdarabolása

A stringet könnyedén szétdarabolhatjuk egy karaktersor mentén a .split() metódussal. Eredményül egy list-et kapunk.

Akkor lehet hasznos ez a metódus, ha struktúrált adatot (pl.: vesszővel tagoltat) olvasunk be. Ilyenkor egyszerűen szétvágjuk a stringet a határoló karakternél és máris indexekkel hivatkozhatunk a struktúra elemeire.

Mondjuk hogy a felhasználó időjárási megfigyeléseket gépel be, minden értéket vesszővel majd szóközzel elválasztva (jó igényesen). Lezsíroztuk vele, hogy először mindig az észlelés időpontját, majd a hőmérsékletet és végül a csapadékmennyiséget gépeli be. Írjunk erre egy egyszerű programot, ami egyelőre csak egy észlelést olvas be, és rögtön kiírja hogy a begépelt adat szerint mekkora volt a hőmérséklet.

weather = input('Az észlelés (időpont, hőmérséklet, csapadék): ')

date, temperature, rain = weather.split(', ')

print('A hőmérséklet', temperature, 'volt')

A weather változóba bekerül a begépelt szöveg, amit a 3. sorban a .split()-el jól szétvágunk mindenhol ahol a ', ' string előfordult.

Tudjuk (oké, reménykedünk hogy a felhasználó pont megfelelő mennyiségű és formátumú adatot gépelt be), hogy egy 3 elemű tömböt kapunk visszatérési értékül, aminek az elemeit rögtön jó Pythonosan három változóba ki is rakunk. Ez után gyerek játék kiírni a hőmérséklet értékét, azaz a temperature-t.

Szövegek egyesítése

Az előző metódus párja, amikor egy tömbnyi szöveget össze egyesítünk.

Hasonlóan, ez akkor hasznos, ha struktúrált adatot jelenítünk meg. Így akkor írjuk meg az előző program ellentettjét, ami kiírja egy tömb elemeit vesszővel és szóközzel elválasztva:

data = ['2016-01-01 00:00:00', '0°C', '0 mm']

weather = ', '.join(data)

print(weather)

A 3. sorban kiadjuk azt a karaktersorozatot, amivel a tömb elemeit egyesítjük, és a .join() paramétere a felsorolás (most a data tömb).

Kicsit szokatlan lehet ez a sor: miért nem a list-nek van .join() metódusa? Elvégre a tömb elemeit egyesítem egymáshoz. A válasz az, hogy nem csak tömböt, hanem tetszőleges felsorolás elemeit lehet egyesíteni, aminek az eredménye mindig string, így ez az str feladata.

Format string - változók és stringek egyesítése

Azóta akartam erről a témáról beszélni, hogy a beolvasásról és kiírásról szóló bejegyzés zárásában ezt leírtam:

print(a, '+', b, '=', a + b)

Az a baj, hogy ez egy hányás. Olyan sok szóköz, meg minden hieroglifa van benne, hogy le kell ülnöm és végig kell olvasnom hogy értelmezhessem. Szerencsére létezik erre a problémára megoldás (több is!).

A format string egy olyan string amiben kihagyunk helyeket, ahova később változókat illeszthetünk be. Tehát úgy működnek mint egy sablon.

Most az új stílusú format stringet (new-style format string) mutatom be. Ahhoz hogy ezt használjuk egyszerűen helyezzük el a stringben a {} (replacement field) karaktereket oda, ahova egy változó (vagy persze tetszőleges kifejezés értékét) akarjuk írni, és a string .format() metódusbán adjuk át argumentumnként a kifejezést. A többi a Python dolga.

Ilyen egyszerű az egész:

a = 1
b = 2

my_string = '{} + {} = {}'.format(a, b, a+b)
print(my_string)

Igen, ilyenkor jó érzés programozni. A .format() 0. argumentuma az 1. {} helyére kerül, az 1. argumentuma a 2. helyére, és így tovább.

Opcionálisan írhatunk a {} közé sorszámokat, amivel .format()-nak adott argumentum sorszámára hivatkozhatunk. A számozás, ahogy a tömböknél is, 0-tól indul.

a = 1
b = 2

my_string = '{0} + {1} = {2}'.format(a, b, a+b)
print(my_string)

Én inkább ezt a módszert preferálom, így a jövőben tőlem ezt a formát láthatjátok majd. Ezen kívül így megtehetjük azt is, hogy két különböző replacement field ugyan arra az argumentumra hivatkozik (magyarul: kétszer jelenjen meg egy változó a kifejezésben):

a = 1
b = 2

my_string = 'a={0}, b={1}. {0} + {1} = {2}'.format(a, b, a+b)
print(my_string)

A másik dolog, ami már számít is, a float kiírása. Mutatom a problémát:

print('{0}'.format(1/3))

Jó lenne, ha nem lenne tele faltól-falig a konzol a 0.3333333 ...-al. Ezt úgy oldhatjuk meg, ha fix szélességűen jelenítjük meg a törtet, és megadjuk a szélességet, azaz hogy hány tizedesjegy látszódjon. Ezt mind megtehetjük magában a format stringben.

Írjuk ki akkor a törtet 2 tizedesjegyik:

print('{0:.2f}'.format(1/3))

A {0:.2f} felel a kiírásért. Vegyük sorjába a részeit:

  • A 0 azt jelenti, hogy a 0. argumentumot írjuk ki. Ebben semmi új nincs
  • A : azt jelenti hogy az argumentum kiírásakor még elvégzünk valamilyen műveletet
  • A . azt jelenti, hogy a tizedesjegyeket formázzuk
  • A 2 azt, hogy 2 tizedesjegy látszódjon
  • Az f pedig hogy a kifejezés megjelentítése (presentation type) fix szélességű

Végezetül, mivel a format stringben a {} karakterek mindig kicserélésre kerülnek, ezért ha azt akarjuk, hogy mégis jelenjen meg a } vagy a } akkor duplázzuk meg a karaktereket így: }} vagy {{.

A format stringet csak azért nevezzük így, mert tartalmaz replacemenet fieldet. Tehát a format string nem egy spéci str, csak ebben a szerepkörben így szokták nevezni.

A jövőben a } karaktert előszeretettel hívom majd bajuszkának.

dict: az érdekes részek

A dict nem felsorolt típus, mivel a kulcsai nem sorfolytonosak (még akkor se, ha úgy osztjuk ki őket). Ebből kifolyólag nem slice-olhatóak.

A dict kulcsai kicsit saját, külön életet élnek a hozzájuk tartozó értékektől. A feljebb felsorolt összes függvény (len(), min(), max(), sorted(), reversed(), sum()) használható vele, viszont ezek mindig a kulcsokon végzik el a műveletet. Ez leggyakrabban akkor esik rosszul, ha a kulcs-érték párokat sorba akarjuk tenni:

names = {'Alice': 1, 'Bob': 2, 'Carol': 3}

print(sorted(names))

Így bizony egy tömb jelenik meg, amiben a kulcsok vannak felsorolva.

Az operátorok (in, not in és del) szerencsére nem okoznak meglepetést. Az in-el tesztelhetjük, a not in-el kizárhatjuk hogy egy kulcs szerepel-e a dict-ben:

names = {'Alice': 1, 'Bob': 2, 'Carol': 3}
is_alice_here = 'Alice' in names

print(is_alice_here)

Ez pont olyan intuitív mint ahogy megszoktuk.

Törlés esetén a teljes kulcs-érték pár távozik a dict-ből:

names = {'Alice': 1, 'Bob': 2, 'Carol': 3}

del names['Alice']

print(names)

Kulcsok rendezettsége

A fenti néhány sor kimenetén feltűnhet, hogy a dict kulcsai nem olyan sorrendben jelennek meg, mint ahogy azt deklaráltuk a forráskódban. A legrosszabb, hogy előfordul hogy két különböző indítása a programnak két különböző sorrendű dict-et eredményez. Ezzel el is jutottunk a dict legfontosabb tulajdonságához:

A dict kulcsainak sorrendje kiszámíthatatlan.

Azaz nem építhetünk kódot arra, hogy milyen sorrendben követik egymást a dict kulcsai, mivel azok kényük-kedvük szerint változtathatják a helyüket.

Metódusok

A fenti tulajdonságokon kívül a dict-nek is sok metódusa van. Jöjjön pár ezek közül:

Kulcs-érték párok bejárása

Korábban a for ciklusnál láttuk már ezt a metódust, az .items()-et. Ez a metódus egy iterálható objektumot eredményez, ami minden iterációval egy két elemű tömböt ad vissza: az első elem a kulcs, a második az érték. Ezzel a metódussal tehát szépen végig járható az egész dict:

A jobb átláthatóság érdekében most ne foglalkozzunk ciklussal, hanem írjuk ki a metódus visszatérési értékét list-ként:

names = {'Alice': 1, 'Bob': 2, 'Carol': 3}

print(list(names.items()))

A konzolban a [('Alice', 1), ('Bob', 2), ('Carol', 3)] sor jelenik meg. Láthatóak tehát a kis két elemű tömbök.

Kulcsok bejárása

Csak a kulcsokat a .keys() metódussal járhatjuk be:

names = {'Alice': 1, 'Bob': 2, 'Carol': 3}

print(list(names.keys()))

Értékek bejárása

Az értékeket egy nagy összefüggő masszaként járhatjuk be a .values() metódussal:

names = {'Alice': 1, 'Bob': 2, 'Carol': 3}

print(list(names.values()))

dict-ek egyesítése

Az .update() metódussal egyesíthetünk két dict-et.

names = {'Alice': 1}

names.update({'Bob': 2, 'Carol': 3})

print(names)

Ez akkor lehet hasznos, ha több új kulcsot akarunk egyszerre hozzáadni egy meglevő dict-hez. Mivel a dict minden kulcsa egyedi, ezért ha átfedés van a régi- és az új kulcsok között, akkor az újat felülírják a régieket.

Megjegyzés a kulcsok és értékek bejárásáról

Logikus gondolat, hogy például azért akarunk végig járni egy dict-et, hogy minden benne levő értékez növeljünk egy számmal.

Írjunk tehát egy programot amiben a named dict kulcsai nevek, az értékei pedig életkorok. Adjunk minden életkorhoz 1-et:

names = {'Alice': 25, 'Bob': 24, 'Carol': 23}

for age in names.values():
    age += 1

print(names)

Futtassuk a programot és érezzük a csalódottságot: a names dict változatlan maradt. Ennek a miértjét a következő bejegyzésben tárgyaljuk, de a lényeg: azzal hogy az age-t növeljük bár növekszik az érték (írj be az age += 1 alá egy print(age)-t és látni fogod), de az age-hez tartozó érték nem változik a names változóban.

Azért hogy az ilyen problémákat elkerüljük ne az értékeket járjuk be, hanem a kulcsokat, és a kulccsal hivatkozzunk a dict értékére:

names = {'Alice': 25, 'Bob': 24, 'Carol': 23}

for name in names.keys():
    names[name] += 1

print(names)

Zárás

Ez a bejegyzés igen vastagra sikeredett, de ezzel a tudással jó ideig vígan elleszünk. A következő részben függvényt fogunk írni, így ott sokkal kevesebb lesz a magolni való, viszont sokkal többet foglalkozunk a Python lelki világával.

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

-slp