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

Nincsenek megjegyzések:

Megjegyzés küldése