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

2016. március 10., csütörtök

05 - Elágazások, ciklusok

Elérkeztünk oda, ami anno 3 éve nem sikerült, és végre új részt prezentálhatok a Python tutorialomból.

Az előző részekben a típusoktól és változóktól eljutottunk egészen odáig hogy képesek voltunk a felhasználótól adatot beolvasni a konzolon keresztül. Most következik az a rész, amikor végre a számítógép is csinálni fog valamit, úgyhogy jöjjenek az elágazások és ciklusok, avagy a vezérlési szerkezetek (control flow).

Elágazások

Azon kívül hogy a számítógép nagyon jól össze tudja adni a számokat, feltételeket is tud vizsgálni, és a vizsgálat eredményének függvényében a program futása különböző utakon haladhat tovább. Ezt hívjuk elágazásnak.

Elágazás létrehozásához az if (ha) utasítást kell használnunk. Nézzük hogy hogyan működik:

number = int(input('Írj be egy számot: '))
if number > 100:
    print('A szám nagyobb mint 100')

A 2. sortól érdekes a történet. Az if utasítás mellett egy olyan kifejezés következik, aminek van logikai értéke. A number > 100 kifejezés, ha a number értéke nagyobb mint 100, akkor True, amúgy False kiértékelési értéket kap. A sort mindig a : zárja, erre figyeljünk.

A 3. sorban valami tök új dolgot látunk, méghozzá hogy ez a sor kicsit beljebb kezdődik mint a többi. A Pythonban ilyen bekezdésekkel (indent, indentation) tudunk kód blokkokat (törzseket) létrehozni. Egyes utasítások, mint az if is, megkövetelik hogy új blokkot nyissunk utánuk. Ilyen utasítás nélkül, csak úgy poénból nem tologathatjuk kifelé-befelé a forráskódot kedvünk szerint, mert IndentationError hibaüzenetet kapunk.

Általában elmondható, hogy amelyik utasítás végére :-t kell írni, az alatt új blokkot kell nyitni.

Futtassuk a kódot és írjunk be egy 100-nál nagyobb számot. Válaszul az A szám nagyobb mint 100 üzenetet kapjuk.

Futtassuk még egyszer a programot, de most írjunk egy 100-nál kisebb értéket. A konzolban semmi se jelenik meg. Láthatóvá vált, hogy az if blokkja akkor kerül futtatásra, ha az if melletti kifejezés True értékű. Lehetőségünk van szerencsére rögtön, ugyan ebben a szerkezetben kezelni a False értéket is, ehhez az else (különben) utasítást kell használni.

number = int(input('Írj be egy számot: '))
if number > 100:
    print('A szám nagyobb mint 100')
else:
    print('A szám kisebb mint 100')

Az else és az if egy párt alkotnak, ezért azonos bekezdést kell alkalmazni előttük (azonos bekezdési szintent (indentation level) kell lenniük).

Futtassuk így a programot. Így akár 100-nál nagyobb, akár kisebb értéket írunk be, mindig kapunk valamilyen eredményt, viszont ha pont 100-at írunk be, akkor azt kapjuk hogy A szám kisebb mint 100, ami egyáltalán nem igaz, ezért jó lenne ezt az esetet is kezelni.

Az if és else kombinálató egy 3. utasítással, ez az elif (különben ha).

number = int(input('Írj be egy számot: '))
if number > 100:
    print('A szám nagyobb mint 100')
elif number == 100:
    print('A szám pont 100')
else:
    print('A szám kisebb mint 100')

Ez a program végre tényleg jól működik. Vegyük végig soronként hogy mi történik néhány konkrét érték esetén:

  • Ha 99-et írunk be, akkor az első feltétel (number > 100) hamis lesz, így ugrunk a következőre (number == 100), de ez is hamis. Nincs több elif ág, ezért mindenképp az else-ben kötünk ki.
  • Ha 100-at írunk be, akkor az első feltétel (number > 100) hamis lesz, így ugrunk a következőre (number == 100) ami igaz lesz, így bekerülünk ebbe az ágba.
  • Ha 101-et írunk be, akkor az első feltétel (numer > 100) rögtön igaz lesz, így nem is vizsgálódunk tovább.

Ebben a három pontban igyekeztem nyomatékosítani, de leírom külön is, hogy ha a program futása bekerült valamelyik elágazás blokkjába, akkor a blokk végével a többi feltétel már nem értékelődik ki, hanem véget ér a teljes szerkezet vizsgálata. A következő kód jól mutatja a feltételek sorrendjének fontosságát:

number = int(input('Írj be egy számot: '))

if number > 100:
    print('A szám nagyobb mint 100')
elif number > 200:
    print('A szám nagyobb mint 200')
elif number > 300:
    print('A szám nagyobb mint 300')

Ha beírjuk azt hogy 301, akkor azt kapjuk válaszul hogy A szám nagyobb mint 100, pedig az első elif ág is igaz lett volna… meg a második is… viszont az if ág előbb volt igaz. Mindezekből következik, hogy ha feltételeket írunk akkor legelőször érdemes a legszűkebb esetet írni, és ahogy haladunk lefelé, úgy lazítunk a megszorításokon.

Az elágazási szerkezetet tehát mindig if-el kell kezdeni, tetszőlegs számú elif lehet benne, és igény szerint lehet benne else, ami mindig a legutolsó ága a szerkezetnek.

Kód blokkok

Azt már tudjuk, hogy kód blokkot bekezdéssel hozunk létre. A bekezdéseinket mindig 4 db szóközzel toljuk beljebb (ahogy a fenti példáimban is tettem). A blokk mindig addig tart ameddig azonos bekezdésű sorok követik egymást. Az üres sorok nem zárják le a blokkot, és nem kell beléjük felesleges szóközöket írni.

Ezen a szellős kódon ez jól látszódik:

number = int(input('Írj be egy számot: '))

if number % 2 == 0:
    print('A szám páros')

    print('A szám fele:', number / 2)
else:
    print('A szám páratlan')

    print('A szám kétszerese:', number * 2)

print('Ezt gépelted be:', number)

Akár kevesebb vagy több szóközt, sőt, szóköz helyett tabulátort is használhatunk, de a programozók nagy többsége, és a Python nyelv fejlesztői is a 4 db szóköz használatát ajánlják.

Ciklusok

A programozási nyelvekben a ciklus (loop) azt jelenti, hogy a számítógép valamilyen feltétel függvényében egy blokknyi kódot folyamatosan újra és újra lefuttat.

A Python nyelvben kétféle ciklus létezik: a while és a for.

A while ciklus

A while (amíg) utasítás klasszikusan valósítja meg azt a definíció-szerűséget, amivel ezt a fejezetet bevezettem: addig futtatja a blokkjában szereplő kódot, amíg a neki adott kifejezés logikailag igaz értékű. Lássuk az ide vágó példát is: írjuk ki a számokat 1-10-ig.

counter = 1
while counter <= 10:
    print(counter)
    counter = counter + 1

Az 1. sorban deklaráltuk a counter változót 1 értékkel.

A 2. sor az érdekes: a while melletti feltételétől függ, hogy meddig kell a while blokkját ismételtetni. Ezt magyarul bentmaradási feltételnek szokták hívni.

Nézzük lépésenként hogy mi történik innentől:

  1. A program futása a while counter <= 10-re került. Itt a counter értéke 1, így a kifejezés egyenlő a 1 <= 10-el, ami True-ra értékelődik ki. Így belépünk a blokkba.
  2. Kiírjuk a counter értékét.
  3. A counter változ értékét növeljük 1-el. Ez nagyon fontos.
  4. Véget ért a blokk, vissza ugrunk a while sorára.
  5. A counter értéke most már 2, így a kifejezés egyenlő a 2 <= 10-el, ami True-ra értékelődik ki. Így újra belépünk a blokkba.

Ez a fenti játék egészen addig fut, amíg a counter el nem éri a 11-et, akkor ugyanis:

  1. A counter értéke 11, így a kifejezés egyenlő 11 <= 10-el, ami False-ra értékelődik ki, így nem lépünk be a blokkba.
  2. A blokkon kívüli forráskóddal folytatódik a program.

Látható volt, hogy a while kifejezése még ez előtt kiértékelődött mielőtt beléptünk volna újra a blokkba. Ezért nem jelent meg a 11 a konzolban. A ciklust e tulajdonsága miatt előltesztelő ciklusnak hívják.

Azt amikor egyszer lefut a ciklus blokkja iterációnak (iteration) hívják.

Írjunk egy olyan programot, ami addig olvas be és ad össze tört számokat, amíg egy üres Enter-t nem nyom a felhasználó.

sums = 0

next_number = input('Írj be egy számot: ')
while next_number != '':
    sums = sums + float(next_number)
    next_number = input('Írj be egy számot: ')

print('Az összeg:', sums)

A sums változóban tároljuk majd a részösszeget, (tehát minden iterációban ehhez a változóhoz adjuk majd a begépelt számokat). Ez után kiírjuk az Írjon be egy számot: üzenetet, és a begépelt szöveget eltároljuk a next_number változóban, majd következik is a ciklus.

A feltétel szerint addig ismétlődik a ciklus, amíg a next_number értéke nem üres string. Ilyet akkor kapunk, a nem írunk be semmit, csak egy üres Enter-t nyomunk. Ha rögtön ezzel kezdett a felhasználó, akkor be se lépünk a ciklusba hanem szépen elballagunk mellette. Viszont ha begépelt valamit (akármit), akkor már bent is vagyunk a blokkban.

Itt float-á alakítjuk a next_number értékét, és ezt hozzá adjuk a sums változóhoz, így növeltük a részösszeget (azért használunk float-ot, hogy tört számokkal is működjön a program). A ciklus vége a lényeg: itt szólítjuk fel újra a felhasználót, hogy gépeljen be egy számot, és az eredményt ugyan abba a next_number változóba írjuk, ahova a cikluson kívül is tettük. A futás vissza ugrik a while sorára, ezzel véget ért egy iteráció. Innen pedig az utoljára begépelt értéktől függően vagy belépünk újra a ciklusba, vagy kihagyjuk és megérkezünk az összeg kiírásához.

Ciklus megszakítása, új iteráció indítása

Létezik két speciális utasítás, amit csak ciklusok törzsében lehet csak használni: a continue és a break.

A continue utasítás hatására azonnal új iterációt indít a ciklus. Ez például akkor lehet hasznos, ha át akarunk ugrani egy értéket.

counter = 0

while counter <= 10:
    counter = counter + 1

    if counter % 2 == 0:
        print(counter, 'páros')
        continue
    print(counter, 'páratlan')

Ez egy igen béna kis példa, de a lényeg benne van: ha a counter változó értéke 2-vel osztható, akkor kiírjuk hogy páros és indítjuk is az új iterációt. Ha az érték páratlan, akkor sosem lépünk be a feltétel törzsébe, és megjelenik a páratlan felirat.

A break utasítás hatására azonnal kiugorhatunk a ciklusból Ez akkor jön jól, ha még az előtt be akarnánk fejezni a ciklust, hogy az természetesen (értsd: a feltételében megadott kifejezés függvényében) befejeződne. Általában végtelen ciklus esetén van erre szükség mert hát amúgy… végtelen sokáig menne a ciklus.

A végtelen ciklus

Előfordulnak olyan szituációk, amikor véletlenül vagy direkt olyan kifejezést írunk, ami örökké igaz lesz. Ilyenkor a ciklus soha sem áll le; ezt hívják végtelen ciklusnak (infinite loop). Vegyük a legelső példa kódot ebből a részből. Ha a végéről elhagynánk a counter növelését, akkor a ciklus feltétele örökké igaz lenne, hisz az 1 mindig kisebb a 10-nél; így a program örökké és koxon azt írná a konzolba hogy 1.

Ez elsőre tök haszontalannak tűnhet, de ha egyszerűen nem tudjuk hogy mikor kéne véget érnie a ciklusnak, akkor a legjobb egy végtelen ciklust indítani. Tipikusan ilyen eset az, amikor a felhasználótól várunk valamilyen értéket (vagy a hálózatról).

Vegyük a korábbi példát. Az igaz hogy nincs benne végtelen ciklus, de sajnos az a gond vele hogy redundáns (ismétlés van benne): az input() kétszer, ráadásul azonos üzenettel van meghívva benne.

Úgy is megközelíthettük volna a problémát, hogy mivel nem tudjuk hogy mennyi számot gépelnek majd be, ezért egy végtelen ciklust indítunk, és minden iterációban felszólítjuk a gépelésre a felhasználót. Viszont ha a begépelt érték egyszer egy üres string lesz, akkor jól kiugrunk a ciklusból.

sums = 0

while True:
    next_number = input('Írj be egy számot: ')
    if next_number == '':
        break
    else:
        sums = sums + float(next_number)

print('Az összeg:', sums)

A while True: sor egy gyakran visszatérő “jelenség” Pythonban. Végtelen ciklusokat általában így szoktak indítani (természetesen állhatna it 1 == 1, 1 < 2, stb, de tartsuk magunkat a többiekhez).

A ciklus törzsének nagy része szerintem nem érdemel magyarázatot. A break utasításnál, bár egy feltételben szerepel, de a feltétel a ciklusban van, így ide írható ez az utasítás. Az értelmezésével pedig természetesen a ciklusból, és nem a feltételből ugrunk ki (olyat nem lehet csinálni).

Ezt a mintát (pattern) használják a Pythonban a hátultesztelő ciklusok szimulálására, mert nem létezik külön szerkezet erre az esetre.
Hátultesztelő ciklusnak azt nevezzük, amikor a ciklus törzse fut le először, és csak utána döntjük el, hogy következhet-e iteráció.

Azon kívül hogy mindenki a while True-val indít végtelen ciklust, ennek a kifejezésnek a kiértékelése nem terheli a CPU-t.

A break-es példám annyira nem, de a continue eléggé izzadságszagú, nem véletlenül: általában ki lehe kerülni a break és continue használatát.

Tömb elemeinek kiírása

Zárásul írjunk egy ciklus ami kiírja egymás alá egy tömb elemeit:

names = ['Alice', 'Bob', 'Carol']

index = 0
while index < len(names):
    print(names[index])
    index = index + 1

Újdonság a 4. sorban lehet: a len() függvény. Ez a függvény visszaadja a neki adott objektum hosszát. list esetén ez az elemek száma. Ha tudjuk hogy mennyi elem van a tömbben, akkor nagyon egyszerűen indítunk egy ciklus, ami egyesével elszámol addig, ez kerül az index változóba, és ezzel a változóval indexszeljük a tömböt. Ennyi az egész.

Talán mire leíram a forráskódot már a ti fejetekben is összeállt a probléma megoldása, aminek nagyon örülök, és tök jó. Pythonban viszont soha, de soha nem így járjuk be a tömb elemeit. Hadd mutassam meg nektek a valóságot.

A for ciklus

A for ciklus a Python programozási nyelvben úgynevezett iterálható objektumok (iterables) bejárására (iterálására, iterate) való. Azok az objektumok amiken értelmezve az index operátor általában iterálatóak is. Ilyen a list, a dict és a str is.

Akik más programozási nyelvekből már ismerik a for ciklust, azoknak ez szokatlan lehet.

Vegyük a legutolsó példát: írjuk ki egy tömb elemeit for ciklussal:

people = ['Alice', 'Bob', 'Carol']

for name in people:
    print(name)

Azt a kurva azért ez nem kicsit szexibb. A for ciklus feje két részből tevődik össze:

  1. A for és in között szerepel annak a változónak (vagy változóknak!) a neve, amin keresztül el akarjuk érni az aktuális elemet (ez a példában a name nevet kapta). Itt nem szerepelhet másféle kifejezés.
  2. Az in után következik az objektum neve (vagy tetszőleges kifejezés), amit iterálunk (a példában ez a people változó)

Iteráljunk egy olyan list-et, ami list-eket tartalmaz.

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

for name in people:
    print(name)

A képernyőn kis két elemű tömbök jelennek meg, tehát a for nem mászik bele a belső tömbökbe.

A Pythonban lehetőség van rá, hogy egy tömb elemeit egy sornyi változóba kicsomagoljuk, ez az unpacking. A módszer a következő:

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

for year, name in people:
    print(year, name)

Így az első iterációban a [1990, 'Alice']-ból az 1990-et a year, az 'Alice'-t pedig a name változóba vezethetjük, és így tovább a későbbi iterációk során.

Nézzünk meg egy str-t is:

for c in 'Python':
    print(c)

A str iterálása során karakterenként járjuk végig az objektumot. Tehát a képernyőn egymás alatt jelennek meg a Python szó betűi.

Nézzünk egy dict-et is:

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

A dict iteráláskor a dict kulcsait járja be a ciklus. Azért hogy látszódjanak a kulcsokhoz tartozó értékek is, kézzel meg kellett indexelni people objektumot.

Lehetséges azonban az is, hogy egyszerre iteráljuk a kulcsot és az értéket is. Ehhez a kód:

people = {1990: 'Alice', 1991: 'Bob', 1992: 'Carol'}
for year, name in people.items():
    print(year, name)

A year, name bizonyára már ismerős. A people.items() viszont tök új. Ez egy úgynevezett metódus (method), ami párokban vissza adja a people elemeit, e miatt írhatunk unpackinget a for és in közé.

Erről a metódus témáról egyenlőre ennyit elég tudni, de sejthető, hogy ez valami függvény-szerűség, hisz nagyon hasonlít rájuk. Nagyon sokat fogunk még -nem is olyan túl sokára- beszélni erről.

A for is csak egy ciklus, így használhatóak benne a break és continue utasítások.

A range()

Levezetésnek bemutatnék egy új függvényt, a range()-et. Ez a függvény szám-intervallumok generálására való.

A while-os példákban többször is használtuk a counter változót, amit deklarálni és növelni kellett, más különben nem működött volna a ciklusunk normálisan. Ha ilyen számlálós ciklusokat akarunk írni, akkor a for-al és a range()-el gyorsan és kényelmesen letudhatjuk a problémát.

Írjuk ki így a számokat 1-10-ig:

for counter in range(1, 11):
    print(counter)

Igen, ennyi az egész.

A függvény 3 féle argumentumot tud fogadni, de mindig csak int értékek használhatóak. Itt egy pár példa az összes esetre:

  1. range(5): Előállítja a számokat 0-4-ig. Olyan mint ha azt írnánk hogy [0, 1, 2, 3, 4].
  2. range(5, 10): Előállítja a számokat 5-9-ig. Olyan mint ha azt írnánk hogy [5, 6, 7, 8].
  3. range(5, 10, 2): Előállítja a számokat 5-9-ig kettesével. Olyan mint ha azt írnánk hogy [5, 7].

A 3. argumentum lehet negatív szám is, ilyenkor visszafelé számolunk. Természetesen ekkor az első értéknek nagyobbnak kell lennie a másodiknál:

for i in range(10, 0, -1):
    print(i)

Így tudunk 10-től 1-ig vissza számolni egyesével.

Ha print()-el kiírjuk a range() függvény visszatérési értékét, akkor meglepő módon azt kapjuk a konzolba hogy range(). Ez azért van, mert valójából a range() nem tér vissza semmilyen list-el, amit a for iterálhat, hanem ő maga egy olyan objektum, ami minden iterációban más értéket produkál. Majd mi is írunk ilyet csodát nemsokára.

Zárás

Azt, amikor olyan programot írunk, amiben egymást követő utasítások, elágazások és ciklusok vannak, struktúrált programozásnak (structured programming) nevezik. Python nyelv más stílusú programozást is támogat, mint pl.: a funkciónálist vagy az objektum-orientáltat. Azzal viszont hogy idáig eljutottunk, nem csak a Python, de a legtöbb programozási nyelv alapjait is megismertük.

A következő fejezetben mélyítjuk a Pythonos típusokról szerzett tudásunkat úgy, hogy azokat hasznosítani tudjuk a ciklusokban és elágazásokban.

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

-slp