Arcade-pelien ohjelmointi
Pythonilla ja PygamellaChapter 9: Funktiot
9.1 Mitä funktiot ovat?
Funktioita käytetään kahdesta syystä. Ensinnäkin, koodi saadaan helppolukuisemmaksi ja ymmärrettävämmäksi. Toisekseen, koodia ei tarvitse aina kirjoittaa uudelleen suoritettaessa samoja toimintoja (algoritmeja) uudelleen toistuvasti.
Alla olevassa kuvassa 9.1on esitetty puun piirtämiseen tarvittavat koodit. Siinä toistetaan samaa komentoa useampaan kertaan:
pygame.draw.rect(screen, BROWN, [60, 400, 30, 45]) pygame.draw.polygon(screen, GREEN, [[150, 400], [75, 250], [0, 400]]) pygame.draw.polygon(screen, GREEN, [[140, 350], [75, 230], [10, 350]])
Koodista ei ole helposti pääteltävissä, että piirrettävänä on puu. Jos pitäisi piirtää useita puita tai monimutkaisempia objekteja, tulee koodin ymmärtäminen entistä vaikeammaksi.
Määrittämällä funktion saadaan koodi huomattavasti helpommaksi lukea. Funktion määrittäminen aloitetaan varatulla sanalla def. Tämän def sanan (komennon) jälkeen kirjoitetaan funktion nimi. Tässä kohtaa annamme nimeksi draw_tree. Funktion nimeamiseen pätee samat säännöt kuin muuttujien tunnusten nimeämisessä oli aiemmin.
Funktion nimen jälkeen tulee kaarisulkupari ja määrittely päätetään kaksoispisteeseen. Kaikki funktioon kirjoitettavat komennot tulee sisentää (vrt. for, while, if). Katso alla oleva esimerkki:
def draw_tree(): pygame.draw.rect(screen, BROWN, [60, 400, 30, 45]) pygame.draw.polygon(screen, GREEN, [[150, 400], [75, 250], [0, 400]]) pygame.draw.polygon(screen, GREEN, [[140, 350], [75, 230], [10, 350]])
Tuollaisenaan yllä oleva koodi (funktio) ei puuta piirrä. Se ainoastaan kertoo koneelle how funktion draw_treetulee puu piirtää. Piirtämistä varten funktiota tulee kutsua ohjelmakoodissa, jolloin kutsun jälkeen suoritetaan puun piirtäminen:
draw_tree()
Käyttämällä funktioita voitaisiin lopullinen ohjelma saada näyttämään alla olevan koodin näköiseltä helposti luettavalta koodilta:
draw_tree() draw_house() draw_car() draw_killer_bunny()
Muista, että draw_tree sisälsi kolme riviä koodia. Kukin näistä funktioista, esimerkiksi draw_house, sisältää useita rivejä koodia. Funktioita käyttämällä voimme toistaa funktiokutsuja useita kertoja kirjoittamatta koodia aina uudelleen. Näin ohjelmatkin tulevat lyhyemmiksi.
Funktion nimet ovat tärkeitä. Jos funktio nimetään kuvaavalla nimellä, niin jopa ohjelmointitaidoton voisi ymmärtää, mitä funktion on tarkoitus tehdä. Funktion niemämisessä pitää noudattaa samoja sääntöjä kuin muuttujien nimeamisessä. Eli aloitetaan pienellä kirjaimella, ei numeroa ensimmäiseksi merkiksi, ei erikoismerkkejä. .
9.2 Funktion parametrit
Funktioissa voidaan käyttää parametreja. Parametreilla saadaan funktiosta yleiskäyttöisempiä ja niitä voidaan käyttää erilaisten 'variaatioiden' toteuttamiseen. Esimerkiksi draw_tree() funktio piirtää puun tiettyyn paikkaan. Voimmekin siis lisätä funktioon parametrin, jolla saadaan määrättyä mihin puu tulisi piirtää. Esimerkiksi draw_tree(screen, 0, 230) piirtäisi puun (x, y) paikkaan (0, 230).
Muotoillaan funktion määritys uudelleen seuraavaksi:
def draw_tree(screen, x, y): pygame.draw.rect(screen, BROWN, [60+x, 170+y, 30, 45]) pygame.draw.polygon(screen, GREEN, [[150+x,170+y],[75+x,20+y], [x,170+y]]) pygame.draw.polygon(screen, GREEN, [[140+x,120+y], [75+x,y], [10+x,120+y]])
Nyt saataisiin piirrettyä puita eri paikkoihin. Esimerkiksi:
draw_tree(screen, 0, 230) draw_tree(screen, 200, 230) draw_tree(screen, 400, 230)
Tässä on toinen esimerkki funktiosta, jossa ei ole grafiikkaa. Tämä funktio laskee ja tulostaa pallon tilavuuden:
def volume_sphere(radius): pi = 3.141592653589 volume = (4 / 3) * pi * radius ** 3 print("The volume is", volume)
Funktion nimi on volume_sphere. Funktion tarvitsema lukuarvo on tallennettu radius muuttujaan (parametriin), jota käytetään tuloksen laskemiseen. Pallon tilavuus tulostetaan näytölle. radius muuttujalle ei aseteta arvoa funktiossa, vaan se välitetään funktion kutsussa. Tämä saattaa hämmentää aluksi, koska parametrimuuttujille ei anneta arvoa funktion määrittelylauseessa ja näyttää sääntöjen vastaiselta muuttujan määrittelyltä. Parametrit saavat arvon, kun funktiota kutsutaan.
Funktiota kutsutaan funktion nimellä ja mahdolliset parametriarvot kirjoitetaan kaarisulkeisiin, esim:
volume_sphere(22)
radius muuttuja luodaan ja asetetaan arvoon 22. Funktion koodi suoritetaan kerran ja ohjelman suoritus jatkuu funktion kutsusta seuraavalta riviltä.
Miten funktiolle voidaan völittää useamoia kuin yksi arvo? Funktiolle voidaan välittää useita parametriarvoja. Parametrit erotetaan pilkulla:
def volume_cylinder(radius, height): pi = 3.141592653589 volume = pi * radius ** 2 * height print("The volume is", volume)
Tätä funtiota kutsuttaisiin:
volume_cylinder(12, 3)
Parametrien järjestys kutsussa vastaa järjestystä funktion määrittelyssä. radius saa arvon 12 ja height saa arvon 3.
9.3 Arvon palautus
Esimerkkien funktioissa on yksi harmittava rajoitus. Jos haluaisimme laskea volume_cylinder funktiolla esimerkiksi kuuden sylinterin tilavuuden, niin se ei onnistu. Se tulostaa vain yhden sylinterin tilavuuden. Funktion tulosta ei voida käyttää osana lauseketta, jossa voisi laskea useamman sylinterin tilavuuden kertomalla tilavuus lukumäärällä. (Kääntäjän huom: Funktiota, joka ei palauta mitään arvoa, kutsutaan tarkemmin ottaen 'proseduudiksi' (eng. procedure). Yleisesti funktio palauttaa aina jonkin arvon.)
Tämä ongelma on korjattavissa return lauseella. Esimerkiksi:
def volume_cylinder(radius, height): pi = 3.141592653589 volume = pi * radius ** 2 * height return volume
Return ei ole funktio ja siksi siinä ei käytetä kaarisulkeita. älä siis kirjoita return(volume).
Lisäämällä funktioon return lause, voidaan sen palauttamaa arvoa käyttää osana lausekkeessa, jolla kuuden sylinterin tilavuus saadaan laskettua.
six_pack_volume = volume_cylinder(2.5, 5) * 6
Funktion kutsuun volume_cylinder palautuu return lauseen palauttama arvo ja se kerrotaan luvulla 6.
Tässä on melkoinen toiminnallinen ero funktioilla (proseduurilla), joka tulostaa arvon ja funktiolla, joka palauttaa return lauseella arvon. Katso alla olevaa koodia ja kirjoita se myös itse:
# Funktio (proseduuri), joka tulostaa arvon def sum_print(a, b): result = a + b print(result) # Funtio, joka palauttaa arvon def sum_return(a, b): result = a + b return result # Tämä tulostaa summan 4+4 sum_print(4, 4) # Ja tämä taas ei sum_return(4, 4) # Tämä ei aseta muuttujaan x1 summaa # vaan saa arvon 'None' x1 = sum_print(4, 4) # Tässä taas x2 saa funktion palauttaman arvon x2 = sum_return(4, 4)
Ensimmäisiä funktioita koodatessa, seuraava koodi voisi olla varsin tyypillinen esimerkki:
def calculate_average(a, b): """ Calculate an average of two numbers """ result = (a * b) / 2 return result # Tässä on esimerkiksi muuttujan asetukset x = 45 y = 56 # Mutta, miten saan tulostettua tuloksen? calculate_average(x, y)
Miten saan tulostettua funktion calculate_average palauttaman arvon ? Ohjelmahan ei voi tulostaa result muuttujan arvoa, koska se on funktion sisällä. Tässä pitääkin sijoittaa funktion palauttama arvo muuttujaan:
def calculate_average(a, b): """ Calculate an average of two numbers """ result = (a * b) / 2 return result # Muuttujille arvot x = 45 y = 56 average = calculate_average(x, y) print(average)
9.4 Funktioiden dokumentointi
Pythonin funktioissa on tyypillisesti kommentti ensimmäisenä. Kommentti on kirjoitettu kolmoislainausmerkkien sisään. Sitä kutsutaan docstringiksi. Se voisi olla esimerkiksi:
def volume_cylinder(radius, height): """Palauttaa sylinterin tilavuuden, kun säde on annettu.""" pi = 3.141592653589 volume = pi * radius ** 2 * height return volume
Docstringin käytössä on varsin mielenkiintoista se, että kommentti voidaan tulostaa web-sivulle ja näin funktio saadaan dokumentoitua ilman ylimääräistä kirjoittamista. Eräs Python dokumenttigeneraattori on Sphinx. Monissa ohjelmointikielissä on samanlainen työkalu, jolla dokumentaatio saadaan generoitua suoraan koodin kommenteista. Tämä säästää todella paljon aikaa, varsinkin suurempien ohjelmien kanssa.
9.5 Muuttujien näkyvyys
Funktioiden käytössä törmätään käsitteeseen muuttujan näkyvyys. Näkyvyydellä tarkoitetaan sitä, milloin muuttuja on olemassa ja sen arvo on saatavilla. Esimerkiksi,katso alla olevaa koodia:
# Määritellään yksinkertainen funktio, jossa # x saa arvon 22 def f(): x = 22 # Kutsutaan funktiota f() # Seuraava tulostus ei toimi, koska x on olemassa vain funktiossa f(), ei pääohjelmassa print(x)
Viimeinen rivi generoi virheilmoituksen, koska muuttujaa x ei ole määritelty pääohjelmassa ja sen 'on olemassa' vain funktion f() suorituksen ajan. Eli muuttuja x luodaan, kun ohjelma siirtyy f() funktioon. Funktion käyttämä muistitila vapautetaan, kun f() funktion suoritus päättyy ja samalla päättyy muuttujan x 'elämä'.
Monimutkaisemmaksi asia menee, kun käsittelemme funktion muuttujia ulkopuolella f() funktiota. Seuraavassa koodissa, x luodaan ennen kuin siirrytään f() funktioon ja siksi se voidaan printata f() funktion sisällä.
# Create the x variable and set to 44 x = 44 # Define a simple function that prints x def f(): print(x) # Call the function f()
Muuttujia, jotka on luotu funktion ulkopuolella (pääohjelmassa), voidaan tulostaa funktion sisällä vain, jos niiden arvoa ei muuteta funktiossa. Seuraava samantapainen koodi kuin yllä, tuottaa virheen. Virheilmoitus tulee siitä, että muuttujaa x ei tunneta.
# Create the x variable and set to 44 x = 44 # Define a simple function that prints x def f(): x += 1 print(x) # Call the function f()
Joissakin kielissä on vieläkin monimutkaisemmat säännöt muuttujien luomisen ja näkyvyyden osalta verrattuna Pythoniin, jossa tämäkin asia on melko suoraviivainen ja aloittelijan ymmärrettävissä.
9.6 Arvon palautus funktiosta
Funktio palauttaa arvon luomalla kopion alkuperäisestä muuttujasta. Esimerkiksi:
# Määritetään yksinkertainen x:n tulostava funktio def f(x): x += 1 print(x) # Asetetaan arvo y y = 10 # Kutsutaan funktiota f(y) # Tulostetaan y ja katsotaan onko muuttunut? print(y)
Muuttujan y arvo ei muutu. Funktio f() kasvattaa siis y:n kopion arvoa, mutta ei alkuperäistä muuttujaa y. Funktiossa listatuista parametreistä luodaan kokonaan uudet muuttujat. Funktion kutsussa annettu parametrin arvo kopioidaan tähän uuteen muuttujaan, silloin kun funktiota kutsutaan.
Edellä ollut esimerkki on varsin toimiva ja järkeenkäypä. Asia menee kuitenkin sekavammaksi, jos funktion kutsussa parametrin ja funktiossa muuttuijan nimet ovat samat. Koodin ymmärätminen menee hankalaksi vaikka se periaattessa toimii edelleenkin samalla tavalla. Seuraava esimerkki on aivan sama, paitsi y muuttujan tunnuksena on x.
# Määritetään yksinkertainen tulostusfunktio def f(x): x += 1 print(x) # Asetetaan x:n arvo x = 10 # Kutsutaan funktiota f(x) # Katsotaan, onko x-muuttujan arvo muuttunut print(x)
Tulos on aivan sama kuin käyttäessämme muuttujana ytä. Vaikka molemmissa, sekä funktiossa, että ulommassa koodissa on x muuttujat, ovat ne kuitenkin täysin eri muuttujat. Tässä yhteydessä puhutaan muuttujan näkyvyydestä. Tässä on siis muuttujan sisällä oleva muuttuja x ja se 'näkyy' vain funktion lohkossa. Ulommassa lohkossa oleva muuttuja x on eri muuttuja.
9.7 Funktio kutsuu funktiota
Funktio voi kutsua myös toista funktiota. Seuraavassa esimerkissä on funktiossa kutsu toiseen funktioon. Määritellään ensin kaksi funktiota:
def kasi_eteen(kumpiKasi, kammenYlosTaiAlas): # koodi tulisi tähän def kasi_olkaan(kasi, olka): # koodi tulisi tähän
Sitten voisi olla funktio macarena, joka kutsuu näitä yllä olevia funktioita:
def macarena(): kasi_eteen("oikea", "alas") kasi_eteen("vasen", "alas") kasi_eteen("oikea", "ylos") kasi_eteen("vasen", "ylos") kasi_olkaan("oikea", "vasen olka") kasi_olkaan("vasen", "oikea olka") # etc
9.8 Pääfunktiot ja globaalit muuttujat
Kun ohjelmista tulee laajempia, saadaan koodista helpommin hallittavaa ja selkeää laittamalla koodi funktioihin. Tähän asti koodi on pääasiassa kirjoitettu “sisennystasolle 0.” Koodi on siis tasattuna vasemmassa reunassa emmekä ole käyttäneet paljoakaan funktioita.
Tätä koodausfilosofiaa voisi ajatella siten, kuin laittaisit kaikki vaatteesi samaan kasaan lattialle tai pitäisit kaikki työkalusi samassa laatikossa. Tämä toimii niin kauan kun tavaramäärä ei ole suuri. Silti tämä toimintatapa on melko sotkuista.
Kaikki ohjelmakoodi ja muuttujat tulisi sijoittaa funktioihin. Näin koodista tulee organisoitua ja virheiden 'debuggaaminenkin' onnistuu helpommin. KAikki muuttujat, jotka määritellään (asetetaan) sisennystasolla 0, ovat globaaleja muuttujia. Globaalit muuttujat ovat hankalia. Miksi ovat? Siksi, että missä tahansa koodia mikä tahansa muutos voi muuttaa muuttujan arvoa. Jos ohjelmassa on esimerkiksi 50000 riviä koodia, voi mikä tahansa rivi muuttaa globaalin muuttujan arvoa. Jos laitatkin muuttujan funktioon, voi muuttujan arvoa muuttaa ainoastaan tässä funktiossa. Jos siis ohjelmassa tulisi jokin odottamaton muuttujan arvo, riittäisi tutkia vai esim. noin 50 riviä koodia funktiosta ja tarkistaa muuttujan käsittely ja arvon asetukset. Muutoin sinun pitäisi tutkia kaikki 50000 riviä koodia tässä virheen etsinnässä.
Parempi tapa kirjoittaa Python-koodia voisi olla esimerkiksi tällainen:
def main(): print("Hello world.") main()
Tässä siis kaikki koodi on sijoitettu sisennettynä main funktioon sen sijaan, että olisi kirjoitettu sisennystasolle 0.
Mutta hetkinen! Tässä ilmenee toinenkin ongelma ratkaistavaksi. Tulemme jatkossa oppimaan kappaleessa 14, että ohjelma voidaan 'palastella' useampaan tiedostoon. Saamme näistä tiedostoista funktiot käyttöön import komennolla. Jos käytämme ohjelmassa import komentoa, liittyvät funktiot saadaan tällöin suoritettavaksi vain main funktiossa. Tätä emme tietenkään halua. Toiveena olisi siis, että funktio olisi käytettävissä aina, kun sitä kutsutaan.
Tämä haitta saadaan korjattua Pythonissa globaalilla muuttujalla, joka tarkistetaan tarvittaessa. (Tosiaan, äsken moitimme globaalien muuttujien heikkoja ominaisuuksia.) Tämä muuttujuja on __name__, joka on kirjoitettu kahdella alaviivalla sekä alussa että lopussa. Voimme tarkistaa onko tämä koodi importattu tai ajettavissa. Jos koodi voidaan suorittaa, Python automaattisesti asettaa sen muuttujan arvon muuttujaan __main__. Testaamalla if lauseella, kutsutaan main funktiota ainoastaan silloin, jos koodi on suoritettavissa. Muussa tapauksessa koodi vain määrittää main funktion. Koodi, jossa funktio 'importattiin voi suorittaa funktion.
Kaikki Python koodit pitäisi suorittaa seuraavasti:
def main(): print("Hello world.") if __name__ == "__main__": main()
Pythonin yksi etu aloittelijalle on, että tätä monimutkaiselta vaikuttavaa funktiokutsutapaa ei tarvitse käyttää ennen kuin sitä todella tarvitsee. Muissa kielissä (esim. Javassa) tätä helpoutta ei ole, vaikka ohjelma olisi hyvinkin pieni.
Yksinkertaisuuden vuoksi tässä materiaalissa ei käytetä tätä funktioiden muuttujien käsittelytapaa. Mutta myöhemmin ohjelmakoodisi kasvaessa ja tullessa kompleksisemmaksi tästä on paljon hyötyä. Eihän 'vaatteitakaan kannata heittää lattialle samaan kasaan', kuten edellä oli puhetta.
Jos olet edistynyt taidoiltasi jo niin, että voit aloittaa kirjoittamaan ohjelmasi tällä tavalla, niin anna mennä vaan. Tällä tavalla on myös helpompi havaita, miten ohjelma käsittelee dataa ja miten se näkyy ohjelman lohkoissa.
Tässä on uusi versio PyGame 'template'-tiedostosta, jossa on käytetty tätä ohjelmointitapaa:
programarcadegames.com/python_examples/f.php?file=pygame_base_template_proper.py
9.9 Esimerkkejä
Kaikissa seuraavissa esimerkeissä, ajattele ensin mielessäsi, mitä koodi mahtaa tulostaa. Tarkista sitten olitko kenties oikeassa. Jos et ymmärtänyt koodin toimintaa, vietä vielä lisää aikaa sen ymmärtämiseksi.
# Example 1 def a(): print("A") def b(): print("B") def c(): print("C") a()
# Example 2 def a(): b() print("A") def b(): c() print("B") def c(): print("C") a()
# Example 3 def a(): print("A") b() def b(): print("B") c() def c(): print("C") a()
# Example 4 def a(): print("A start") b() print("A end") def b(): print("B start") c() print("C end") def c(): print("C start and end") a()
# Example 5 def a(x): print("A start, x =",x) b(x + 1) print("A end, x =",x) def b(x): print("B start, x =",x) c(x + 1) print("C end, x =",x) def c(x): print("C start and end, x =",x) a(5)
# Example 6 def a(x): x = x + 1 x = 3 a(x) print(x)
# Example 7 def a(x): x = x + 1 return x x = 3 a(x) print(x)
# Example 8 def a(x): x = x + 1 return x x = 3 x = a(x) print(x)
# Example 9 def a(x, y): x = x + 1 y = y + 1 print(x, y) x = 10 y = 20 a(y, x) print(z)
# Example 10 def a(x, y): x = x + 1 y = y + 1 return x return y x = 10 y = 20 z = a(x, y) print(z)
# Example 11 def a(x, y): x = x + 1 y = y + 1 return x, y x = 10 y = 20 z = a(x, y) print(z)
# Example 12 def a(x, y): x = x + 1 y = y + 1 return x, y x = 10 y = 20 x2, y2 = a(x, y) # Useimmat ohjelmointikielet eivät sallisi tätä asetuslausetta print(x2) print(y2)
# Example 13 def a(my_data): print("function a, my_data = ", my_data) my_data = 20 print("function a, my_data = ", my_data) my_data = 10 print("global scope, my_data =", my_data) a(my_data) print("global scope, my_data =", my_data)
# Example 14 def a(my_list): print("function a, list = ", my_list) my_list = [10, 20, 30] print("function a, list = ", my_list) my_list = [5, 2, 4] print("global scope, list =", my_list) a(my_list) print("global scope, list =", my_list)
# Example 15 # New concept! # Tätä käsitellään tarkemmin kappaleessa 12 def a(my_list): print("function a, list = ", my_list) my_list[0] = 1000 print("function a, list = ", my_list) my_list = [5, 2, 4] print("global scope, list =", my_list) a(my_list) print("global scope, list =", my_list)
""" This is a sample text-only game that demonstrates the use of functions. The game is called "Mudball" and the players take turns lobbing mudballs at each other until someone gets hit. """ import math import random def print_instructions(): """ This function prints the instructions. """ # You can use the triple-quote string in a print statement to # print multiple lines. print(""" Welcome to Mudball! The idea is to hit the other player with a mudball. Enter your angle (in degrees) and the amount of PSI to charge your gun with. """) def calculate_distance(psi, angle_in_degrees): """ Calculate the distance the mudball flies. """ angle_in_radians = math.radians(angle_in_degrees) distance = .5 * psi ** 2 * math.sin(angle_in_radians) * math.cos(angle_in_radians) return distance def get_user_input(name): """ Get the user input for psi and angle. Return as a list of two numbers. """ # Later on in the 'exceptions' chapter, we will learn how to modify # this code to not crash the game if the user types in something that # isn't a valid number. psi = float(input(name + " charge the gun with how many psi? ")) angle = float(input(name + " move the gun at what angle? ")) return psi, angle def get_player_names(): """ Get a list of names from the players. """ print("Enter player names. Enter as many players as you like.") done = False players = [] while not done: player = input("Enter player (hit enter to quit): ") if len(player) > 0: players.append(player) else: done = True print() return players def process_player_turn(player_name, distance_apart): """ The code runs the turn for each player. If it returns False, keep going with the game. If it returns True, someone has won, so stop. """ psi, angle = get_user_input(player_name) distance_mudball = calculate_distance(psi, angle) difference = distance_mudball - distance_apart # By looking ahead to the chapter on print formatting, these # lines could be made to print the numbers is a nice formatted # manner. if difference > 1: print("You went", difference, "yards too far!") elif difference < -1: print("You were", difference * -1, "yards too short!") else: print("Hit!", player_name, "wins!") return True print() return False def main(): """ Main program. """ # Get the game started. print_instructions() player_names = get_player_names() distance_apart = random.randrange(50, 150) # Keep looking until someone wins done = False while not done: # Loop for each player for player_name in player_names: # Process their turn done = process_player_turn(player_name, distance_apart) # If someone won, 'break' out of this loop and end the game. if done: break if __name__ == "__main__": main()
9.10 Review
9.10.1 Multiple Choice Quiz
Klikkaa tästä monivalintatehtävään.
9.10.2 Short Answer Worksheet
Klikkaa tästä kappaleen kertaustehtäviin.
9.10.3 Lab
Klikkaa tästä ohjelmointitehtäviin.
You are not logged in. Log in here and track your progress.
English version by Paul Vincent Craven
Spanish version by Antonio Rodríguez Verdugo
Russian version by Vladimir Slav
Turkish version by Güray Yildirim
Portuguese version by Armando Marques Sobrinho and Tati Carvalho
Dutch version by Frank Waegeman
Hungarian version by Nagy Attila
Finnish version by Jouko Järvenpää
French version by Franco Rossi
Korean version by Kim Zeung-Il
Chinese version by Kai Lin