<h1>A5 Harjoitus</h1> Harjoituksen aiheena ohjelmarakenteet `def`{.py}-avainsanaa käyttäen. Tutustutaan funktioiden nimeämiseen, määrittelyyn, sekä niiden kutsumiseen. Määrittelyyn sisältyy aliohjelmien parametrisointi, sekä paluuarvojen asettaminen. Aliohjelma kutsujen yhteydessä harjoitellaan asettamaan myös tarvittaessa argumentit, sekä tallettamaan aliohjelman paluuarvo. Aiheen esittelyvideo(tulossa): **Sisällysluettelo:** 1. [Virheenjäljitys (Debugging)](#virheenjäljitys-debugging) 2. [Funktion määrittely](#funktion-määrittely) 3. [Parametrit \& argumentit](#parametrit--argumentit) 4. [Paluuarvot](#paluuarvot) 5. [Tyyppivihjeet](#tyyppivihjeet) 6. [Funktion kommentointi](#funktion-kommentointi) 7. [Kutsupino](#kutsupino) 8. [Pää- ja aliohjelmat](#pää--ja-aliohjelmat) 9. [Nimiavaruus](#nimiavaruus) Versio: `1.0.0` # Virheenjäljitys (Debugging) Tutustutaan aluksi virheenjäljitykseen VSCode:ssa. Ohjelmoinnin alkuvaiheessa virheenjäljitystyökalua voi käyttää visualisoimaan ohjelman suoritusta. Seuraamalla suorituksen etenemistä debuggerilla, voidaan tarkistella tarkemmin ohjelma-ajojen aikaisia tapahtumia, sekä tiloja(muuttujien arvoja) muistissa. [youtu.be/2FRMJJjSdXc](https://youtu.be/2FRMJJjSdXc) <iframe width="480" height="405" src="https://www.youtube.com/embed/2FRMJJjSdXc?si=r3s-JI5j3y0IY2is" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> **Videon esimerkki ohjelmakoodi alla:** ```py= # Virheen jäljitys Iterations = 50 while Iterations > 0: print("Iterations left", Iterations) Iterations = Iterations - 1 print("Program ending") ``` # Funktion määrittely Python ohjelmissa funktioiden määrittely aloitetaan avainsanalla `def`{.py}. Tätä seuraa funktion nimen määrittely, joka siis usein määritellään jollakin verbillä ja se kuvailee funktion toimintaa. Funktion nimen perään laitetaan sulkeet `()`, jonka perään nuolinotaatiolla ` -> ` palautettavan arvon tietotyyppi esim. `None`. Ensimmäisen rivin loppuun on laitettava kaksoispiste `:`. Funktiossa tapahtuvat asiat määritellään sisentämällä ja funktiomäärittely päättyy seuraavaan sisentämättömään riviin. Funktiossa voidaan määritellä suoritettavan yksi tai useampi komento ja funktion lopuksi kutsutaan palautuskomentoa `return`{.py}. Jos funktiosta ei ole tarkoitus palauttaa mitään arvoa, voidaan paluuarvoksi määrittää `None`{.py}. Alla on esimerkki pienen funktion määritelmästä. Ohjelmakoodin lopussa kutsutaan `main`-funktiota, joka käynnistää funktion suorituksen. Huomaa sulkeet "`main()`" funktionimen lopussa. Tämä tarkoittaa funktion kutsua, eli kyseisen funktion suoritus käynnistyy. **Esim 2.1 - Pääohjelma(funktio)** ```py= # Function definition starts def main() -> None: print("Program starting.") print("Welcome to the function!") print("This is second command in function.") print("Program ending.") return None # Function definition ends main() # Call "main"-function ``` **Ohjelma-ajo:** ```stdio3 Program starting. Welcome to the function! This is second command in function. Program ending. ``` Yllä olevasta ohjelmasta voi olla vielä haastavaa hahmottaa kuinka Python-tulkki lukee ja suorittaa sitä. Tässä pysyvä linkki [pythontutor.com/render.html](https://pythontutor.com/render.html#code=%23%20Function%20definition%20starts%0Adef%20main%28%29%20-%3E%20None%3A%0A%20%20%20%20print%28%22Program%20starting.%22%29%0A%20%20%20%20print%28%22Welcome%20to%20the%20function!%22%29%0A%20%20%20%20print%28%22This%20is%20second%20command%20in%20function.%22%29%0A%20%20%20%20print%28%22Program%20ending.%22%29%0A%20%20%20%20return%20None%0A%0A%23%20Function%20definition%20ends%0Amain%28%29%20%23%20Call%20%22main%22-function%0A&cumulative=false&curInstr=8&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false) parametreineen sivulle, joka visualisoi yllä olevan esimerkin vaiheet. Klikkaamalla linkkiä välittää linkki siis koodiesimerkin tiedot sivulle. Esimerkin alle pitäisi ilmestyä askellus painikkeet, joita klikkaamalla sivusto näyttää suorituksen eri vaiheet. # Parametrit & argumentit Funktioille voidaan lähettää tietoja argumenttien muodossa. Edellytyksenä tietysti on, että funktioon on määritelty parametrit, jotta funktioon tuleva tieto voidaan käsitellä. Sanat "parametri" ja "argumentti" viittavat usein samaan asiaan funktioiden asiayhteydessä. Yleensä parametri on funktiossa määritelty asia ja paikanpitäjä arvolle, joka lähetetään argumenttina funktiolle sen kutsun yhteydessä. Alla olevassa esimerkissä on muodostettu funktio nimeltä `subtract`. Funktioon on määritelty kaksi liukuluku parametriä - `PMinuend: float`{.py} ja `PSubtrahend: float`{.py}. Funktio laskee lukujen erotuksen, jonka jälkeen tulostaa laskutoimituksen nähtäville. Tämän jälkeen ohjelman suoritus palautetaan funktiosta komennolla `return None`. Ohjelmakoodin lopussa funktiota kutsutaan kahdesti eri argumenteilla. **Esim 3.1 - Parametrisointi** ```py= # Function with two parameters "PMinuend" and "PSubtrahend" def subtract(PMinuend: float, PSubtrahend: float) -> None: Difference = PMinuend - PSubtrahend print(PMinuend, "-", PSubtrahend, "=", Difference) return None # Function fully defined # Calling "subtract" function and passing following arguments # 1st param Minuend = (arg[0]: 5) # 2nd param Subtrahend = (arg[1]: 3) subtract(5, 3) # Calling once more with arguments arg[0]: 3, arg[1]: 5 subtract(3, 5) ``` Ohjelma-ajo: ```stdio 5 - 3 = 2 3 - 5 = -2 ``` Tuloksessa huomattavaa on se, että järjestyksellä on merkitystä. Ensimmäisessä ja toisessa funktiokutsussa on laitettu samat arvot(3 & 5) eri järjestyksessä, jolloin tulos muuttuu. Funktiolle syötettävät arvot tulee määrittää siinä järjestyksessä, kun ne parametreille halutaan sijoitettavan. Funktiokutsussa argumentit erotellaan pilkuilla. Järjestys on vasemmalta alkaen. # Paluuarvot Kaikkien Python funktioiden lopussa palataan(poislukien ikisilmukka tilanne) takaisin sinne, mistä funktiota kutsuttiin. Palautukseen voidaan määrittää arvo `return`{.py}-komennolla, jolla funktiosta poistutaan. Jos palautukseen ei ole tarvetta laittaa arvoa, sopii lopetukseen komento tyhjällä arvolla - `return None`{.py}. Alla olevassa esimerkissä on kaksi funktiota, joista ensimmäinen palauttaa kokonaisluvun ja toinen tyhjän arvon. Huomaa rivin 14 `askChoice`-funktion kutsu ja sen paluuarvon talletus muuttujaan `Digit`. **Esim 4.1 - Paluuarvo** ```py= # Function with actual numeric return value def askChoice() -> int: Choice = -1 Feed = input("Your choice: ") if Feed.isnumeric(): Choice = int(Feed) return Choice # Function with None return value def main() -> None: print("Program starting.") print("Choose a digit 0-9") # Note! catch the return value into a variable Digit = askChoice() if len(str(Digit)) != 1: # not a digit on 0-9 range print("Inserted value is not a digit on 0-9 range") else: print("Your digit was", Digit) print("Program ending.") return None # Note! re main() ``` **Ohjelma-ajo:** ```stdio3 $%# t1-r1 start #%$ Program starting. Choose a digit 0-9 Your choice: a Inserted value is not a digit on 0-9 range Program ending. $%# t1-r1 end #%$ $%# t1-r2 start #%$ Program starting. Choose a digit 0-9 Your choice: 3 Your digit was 3 Program ending. $%# t1-r2 end #%$ ``` # Tyyppivihjeet Python on **dynaamisesti tyypitetty** ohjelmointikieli. Se tarkoittaa, että Python tarkastelee tietotyyppejä ajon aikana. Eli onko jokin muuttuja merkkijono `str`{.py} vaiko `int`{.py} tyylisesti. Tämä antaa paljon joustavuutta Python kehitykseen. Kehittäjinä meidän ei siis tarvitse määrittää tietotyyppejä ennen ohjelma-ajoa. Kolikon kääntöpuolena on, että ohjelman määritykset eivät ole yksiselitteisiä(eksplisiittisiä) ja näin ollen päädymme ohjelmoijina useammin virhetilanteiden havainnointiin vasta ohjelma-ajon aikana. **Staattisesti tyypitetyt** ohjelmointikielet edellyttävät ohjelmoijia aina määrittelemään tiedoille tietotyypit. Tämä vaatimus tekee ohjelmoinnista kankeampaa, mutta mahdollisten virhetilanteiden havainnointi aikaistuu osin aikaan ennen ohjelma-ajo vaihetta. Moni dynaaminen ohjelmointikieli tukee kuitenkin **tyyppivihjeitä**, joiden avulla saavutetaan staattisesti tyypitettyjen kielien hyviä puolia, kuten koodin luettavuus, ylläpidettävyys ja tarkkuus. Tyyppivihjeet eivät kuitenkaan Pythonin tapauksessa rajoita dynaamisen ohjelmoinnin joustavuutta, eli tyyppivihjeiden käyttö on suotavaa lähes poikkeuksetta. Kertauksen vuoksi alle on listattu Pythonin yleisimmät atoomiset tietotyypit, joita tyyppivihjeitä määrittäessä tarvitsemme. Pythonin atoomiset tietorakenteet: - Merkkijono: <span style="color:#216c83;">str</span> = <span style="color:#a31515;">"hi there"</span> - Kokonaisluku: <span style="color:#216c83;">int</span> = -<span style="color:#098658;">2</span> - Liukuluku: <span style="color:#216c83;">float</span> = <span style="color:#098658;">1.23</span> - Boolean(totuusarvo): <span style="color:#216c83;">bool</span> = <span style="color:#00f;">False</span> - Tyhjä(void): <span style="color:#00f;">None</span> = <span style="color:#00f;">None</span> Aiemmissa esimerkeissä kerkesikin jo esiintymään funktion parametrien ja paluuarvojen tyyppivihjeitä. Alle on luotu kaksi lisä esimerkkiä kuvastamaan ohjelmakoodia tyyppivihjeillä(esim 5.1), sekä ilman tyyppivihjeitä(Esim 5.2). **Esim 5.1 - Tyyppivihjeellä** ```py= def evaluateAnswer(PAnswer: str) -> float: Grade = 0 if PAnswer.startswith("Correct"): Grade += 0.5 if PAnswer.endswith("answer"): Grade += 0.5 return Grade MyGrade = evaluateAnswer("Some answer") print("I got grade", MyGrade) ``` **Esim 5.2 - Ilman tyyppivihjeitä** ```py= def evaluateAnswer(PAnswer): Grade = 0 if PAnswer.startswith("Correct"): Grade += 0.5 if PAnswer.endswith("answer"): Grade += 0.5 return Grade MyGrade = evaluateAnswer("Some answer") print("I got grade", MyGrade) ``` Esimerkeissä(E5.1 & E5.2) ainoat erot ovat funktion `evaluateAnswer` parametrin `PAnswer` ja paluuarvon tyypityksessä. Tyyppivihjeen tarkoituksena on selventää tässä tapauksessa tietomuotoa, jota `evaluateAnswer`-funktio ottaa vastaan, sekä palauttaa. Esimerkissä E5.2 ei parametrin perusteella voida olla varmoja odotetaanko arviointi funktioon numeerista arvoa vaiko merkkijonoa. Arvioinneissa käytetään myös eri tapoja, kuten hyväksy-hylkää tai pisteskaala, jolloin kehittäjänä voimme erehtyä odottamaan funktiolta paluuarvona totuusarvoja numeeristen arvojen sijasta. Tilanteen merkitys korostuu, kun ohjelmakoodin määrä lisääntyy. Esimerkissä E5.1 funktion käyttö on kuvattu tyyppivihjeillä. Vastaukseksi(`PAnswer`) kelpuutetaan tyyppivihjeen perusteella merkkijonomuotoista tietoa. Tämä poissulkee muut numeeriset tai oikein/väärin väittämä tyyliset veikkaukset arvon sisällöstä. Paluuarvon tietotyypin mukaan funktiosta palautetaan liukuluku, jolloin voimme jo osittain päätellä, että kyseessä on pisteytys, eikä esim. hyväksy-hylkää periaate. Tässä esimerkissä olisi voitu kuvailla vielä tarkemmin esimerkiksi hyväksyttäviä vastauksen pituuksia taikka arvosana asteikkoja. Tätä kuvailua varten funktioiden määrittelyssä voidaan hyödyntää "docstring":iä, eli funktion kommentointia. # Funktion dokumentointi Hyvä ohjelman osa tai funktio on nimetty selkeästi ja sen tarkoitus ilmenee ilman erillistä dokumentointia. On olemassa paljon eri tilanteita ja tapauksia, joissa ohjelman suorittamaa asiaa on mahdotonta kuvailla tarkoin pelkkien funktionimien tai loogisten operaatioiden ja komentojen avulla. Funktioiden yhteydessä dokumentaatio hoidetaan usein **docstring**:n avulla. Docstring tulee sanoista "documentation string" ja sitä voidaan käyttää ohjelmien, funktioiden, luokkien tai moduulien dokumentointiin. Pythonissa funktion dokumentointi tapahtuu heti koodilohkon alussa käyttäen monirivistä kommenttia. Kommentti alkaa ja päättyy kolmeen lainausmerkkiin `"""`. Näiden väliin tulee funktion kuvaelma kirjoitettuna. docstring:n voi määrittää usealla eri tyylillä Pythonissa: - [Google-style docstring](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) - [reStructuredText (RST)](https://docutils.sourceforge.io/rst.html) - ... Alle on koottu esimerkki Google-tyylisen docstring:n käytöstä funktion määrittelyssä. Esimerkissä määritellään funktiolle kaksi parametriä ja paluuarvo, joiden tyyppivihjeisiin on merkitty liukuluku. Tämän lisäksi parametrien ja paluuarvon sisältö on kuvattu. **Esim 6.1 - Google-style docstring** ```py= def multiply(PMultiplicant: float, PMultiplier: float) -> float: """ This function does multiplication. Args: PMultiplicant (float): Value being multiplied. PMultiplier (float): The value by which the multiplicant is multiplied. Returns: float: Product of the multiplication operation. """ Product = PMultiplicant * PMultiplier return Product multiply(3, 5) ``` Funktiokutsua määriteltäessä VSCode ympäristössä(Python-lisäosa asennettuna), näyttää IntelliSense funktion tyypin(function), parametrit, paluuarvon ja niiden alla docstring sisällön. Alla kuvakaappaus IntelliSensen tarjoamasta ehdotuksesta, kun funktiokutsuun ollaan syöttämässä toista argumenttia. <div style="border: 1px solid gray;"> ![IntelliSense tuo docstring sisällön nähtäville](https://hackmd.io/_uploads/H11bdTaA0.png) </div> *IntelliSense tuo docstring sisällön nähtäville* Tässä kuitenkin hyvä muistaa, että käyttöä on lähinnä demonstroitu yksinkertaistetulla esimerkillä. Selkeästi nimettynä alla vastaava, ilman ylimääräisiä kommentteja: ```py= def multiply(PMultiplicant: float, PMultiplier: float) -> float: Product = PMultiplicant * PMultiplier return Product ``` # Pää- ja aliohjelmat Ohjelmakoodien jäsentely alkaa pää- ja aliohjelmien jaottelusta. Pääohjelma on ohjelma, josta ohjelmakoodin varsinainen suorittaminen aloitetaan. Pääohjelma koordinoi koko ohjelman toimintaa, mutta sen runko kannattaa säilyttää melko kevyenä. Tärkeimmät askeleet pääohjelmassa ovat: 1. Alustaminen - asioiden määrittely ja valmistelu ennen varsinaisia toimenpiteitä. Ilmoitetaan käyttäjälle, että ohjelma käynnistyi. 3. Operointi - ohjelman keskeisien toimintojen toteutusvaihe 4. Siivous - ennen ohjelmasta poistumista tarvittaessa vapautetaan muistia tai tallennetaan asetuksia. Ilmoitetaan käyttäjälle, että ohjelma sulkeutui onnistuneesti Aliohjelmat ratkovat rajattuja tehtäviä ja taustalla on modulaarisuus ajattelu. Aliohjelma on silloin hyvä, kun se suoriutuu sille suunnitellusta tehtävästä hyvin. Aliohjelmalla voidaan esimerkiksi kerätä käyttäjä syöte ja tarkistaa sisältö. Tai vaikkapa suorittaa analyysi. Alla olevassa esimerkissä on näytetty tyypillinen ohjelmarakenne pää- ja aliohjelma jaottelulla toteutettuna. **Esim 7.1 - Tyypillinen ohjelmarakenne** ```py= def subprogram() -> int: print("Subprogram...") print("Returns integer 3") return 3 def main() -> None: # 1. Initialize print("Program starting.") # 2. Operate print("Running subprogram") Value = subprogram() print("Received", Value, "from subprogram.") # 3. Cleanup print("Program ending.") return None main() # Starting main program ``` # Kutsupino Ohjelmien suoritus lähtee liikkeelle pääohjelman kutsusta. Kutsu pääohjelmaan on siis ensimmäinen kutsu joka lisätään **kutsupinoon** (eng. call stack). Pinoon lisättävään kutsuun merkitään paikka, josta funktiota kutsuttiin, eli tiedostopolku, rivinumero ja mahdollisesti moduulin nimi. Ohjelmasuorituksen poistuessa suoritettavasta ohjelmasta, palaa ohjelma kutsupinon perusteella edelliseen paikkaan ja poistaa kyseisen kutsun kutsupinosta. Ohjelmissa kutsupinon käsittely on usein [LIFO periaatteen](https://www.geeksforgeeks.org/lifo-principle-in-stack/) mukainen. Lyhenne tulee englanninkielen sanoista "Last In, First Out". <div> ![lifo](https://hackmd.io/_uploads/HJZTv0TCA.png) </div> Laaja ohjelma voi alkaa kutsumaan useita aliohjelmia. On myös mahdollista, että aliohjelma kutsuu toista aliohjelmaa. Jokainen uusi kutsu **Esim 8.1 - Kutsupino** ```py= import traceback def fnA2() -> None: print("Call stack:") # Display current call stack traceback.print_stack() return None def fnA() -> None: fnA2() # 3rd call -> stack return None def main() -> None: fnA() # 2nd call -> stack return None main() # 1st call -> stack ``` # Nimiavaruus <div style="display:flex;flex-flow:row;justify-content:center;"> ![scope](https://hackmd.io/_uploads/SyE9uhaCA.png =400x400) </div> Terminä nimiavaruus voi kuulostaa alkuun erikoiselle, sillä monilla tulee avaruudesta ehkäpä mieleen ulkoavaruus, kuu, aurinko, tähdet, satelliitit yms. Ohjelmoinnin asiayhteydessä nimiavaruudella kuitenkin tarkoitetaan tilaa, jossa nimetyt asiat ovat. Kun luomme nimetyn muuttujan tai funktion, kuten `Feed`, `Number` tai `welcome`, asettaa Python tulkki sen löydettäväksi kyseisen nimen tai tunnuksen perusteella nimiavaruuteen. Myöhemmin ohjelmakoodissa nimeen viitattaessa osaa tulkki etsiä tietoa tunnuksen perusteella. Nimiavaruuteen liittyy näkyvyystekijöitä (eng. scope), jotka vaikuttavat siihen, että löytääkö Python ohjelmakoodissa määritellyn tunnuksen. <table> <colgroup> <col style="width:33%"> <col style="width:67%"> </colgroup> <thead> <tr> <th>Näkyvyys(scope)</th> <th>Selitys</th> </tr> </thead> <tbody> <tr> <td>Yleinen(global)</td> <td>Tunnukseen voidaan viitata mistä tahansa kohti ohjelmaa.</td> </tr> <td>Paikallinen(local)</td> <td>Tunnus on määritelty paikallisesti ja siihen voidaan viitata ainoastaan paikallisesti(esim. funktion sisällä).</td> </tbody> </table> Tässä kohtaa harjoituksia kaikki määritellyt asiat ovat olleet yleisessä nimiavaruudessa. Pienissä Python ohjelmissa tämä on vielä hallittavissa, mutta ohjelmakoodien kasvaessa nimettyjen asioiden määrä myös lisääntyy. Kun kaksi eri asiaa nimetään ohjelmakoodissa yleiseen nimiavaruuteen samalla nimellä, syntyy törmäys(ylikirjoitus). Seurauksena voi olla, että ohjelma alkaa käyttäytymään ei toivotulla tavalla. Tätä on siis syytä välttää. Funktioita määriteltäessä pääsemme hyödyntämään paikallisia nimiavaruuksia. Muuttuja voidaan nimetä funktioon samalla nimellä, kuin toiseen funktioon ilman törmäyksen riskiä. Samannimiset muuttujat eivät siis ylikirjoita toistensa tietoja ollessaan omissa paikallisissa skoopeissa. Alla pieni esimerkki **Esim 9.1** ```py= def main() -> None: # global Value # Uncomment to change the scope Value = 2 # Local variable print('Value is', Value, 'in "main"-function.') return None Value = 50 # Global variable print('Value is', Value, 'on the top-level.') main() print('Value is', Value, 'after "main" call at the top-level') ``` # Yhteenveto esimerkki Alla perinteisen valikkopohjaisen ohjelman runko. Ohjelma koostuu neljästä eri aliohjelmasta ja pääohjelmasta jolla aliohjelmia koordinoidaan. **Toimintaperiaate tiivistettynä:** 1. Alustusvaiheet 1. Tulkki tallentaa funktiomääritelmät ja käynnistää pääohjelman `main` 2. Pääohjelmaan alustetaan kaksi muuttujaa `Name` ja `Choice` 2. Operointi 1. Valikkoa pyöritetään kunnes `Choice`, eli käyttäjän valinta on nolla `0` 1. Käyttäjälle näytetään valikon vaihtoehdot 2. Käyttäjää pyydetään tekemään valinta ja se tallennetaan `Choice` muuttujaan 3. Valinnan perusteella suoritetaan valikon mukainen toiminto 4. Tulostetaan tyhjä rivi `print("")` jäsentämään valikko toteumaa **Esim 10.1** ```py= # subprogram 1 def showOptions() -> None: print("Options:") print("1 - Ask name") print("2 - Greet user") print("0 - Exit") return None # subprogram 2 def askChoice() -> int: Choice = -1 Feed = input("Your choice: ") if Feed.isnumeric(): Choice = int(Feed) return Choice # subprogram 3 def askName() -> str: Name = input("Insert name: ") return Name # subprogram 4 def sayHello(Name: str) -> str: if Name != "": print("Hi " + Name.capitalize() + "!") else: print("Hey you have not inserted your name yet!") return None # main program def main() -> None: # 1. Initialize Name = "" Choice = -1 # 2. Operate while Choice != 0: showOptions() Choice = askChoice() if Choice == 1: Name = askName() elif Choice == 2: sayHello(Name) elif Choice == 0: print("Exiting program.") else: print("Unknown option!") print("") return None main() ```