Hoofdstuk 61 beschrijft hoe iedere functie een naam heeft, nul of meer parameters, en mogelijk een retourwaarde. Als je je eigen functies maakt, moet je al deze elementen definiëren. Je gebruikt de volgende syntax:
def <functie_naam>( <parameter_lijst> ):
<acties>
Functienamen moeten voldoen aan dezelfde eisen als variabele namen, dat wil zeggen, alleen letters, cijfers, en underscores, en ze mogen niet beginnen met een cijfer. De parameter lijst bestaat uit nul of meer variabele namen, met komma’s ertussen. Het blok code onder de functie definitie moet inspringen.
Let erop dat Python je functie definitie moet hebben “gezien” voordat je
de functie kunt aanroepen. Het is daarom de gewoonte dat functie
definities bovenin een programma staan, meteen onder de import
statements.
Om functies te kunnen creëren, moet je begrijpen hoe Python met functies
omgaat. Bekijk het kleine Python programma hieronder. Dit programma
definieert één functie, die totZiens()
heet. Deze functie heeft geen
parameters. Het blok code behorende bij de functie heeft alleen een
regel die de tekst “Tot ziens!” print.
De rest van het programma is geen deel van de functie. Het deel van de
code van een programma dat niet bij een functie hoort, noemt men meestal
het “hoofdprogramma” (Engels: “main program”). Het hoofdprogramma print
de regel “Hallo!,” en roept daarna de functie totZiens()
aan.
def totZiens():
print( "Tot ziens!" )
print( "Hallo!" )
totZiens()
Als je dit programma uitvoert, zie je dat het eerst de regel “Hallo!”
afdrukt, en daarna “Tot ziens!” Dit gebeurt zo ondanks het feit dat
Python de code van boven naar beneden uitvoert; Python komt
print( "Tot ziens!" )
tegen voordat print( "Hallo!" )
wordt
aangetroffen. De reden dat Python toch eerst de tekst “Hallo!” afdrukt
is dat Python de code van een functie alleen uitvoert als de functie
wordt aangeroepen. Een functie die Python tegenkomt voordat de functie
wordt aangeroepen wordt alleen geregistreerd – Python onthoudt dat die
functie bestaat, zodat hij kan worden uitgevoerd als het hoofdprogramma
(of een andere functie) de functie aanroept.
Bekijk de code hieronder. De code definieert een functie hallo()
met
één parameter, naam
geheten. De functie gebruikt naam
in het blok
code. Er is geen expliciete assignment die naam
een waarde geeft.
naam
bestaat als variabele naam omdat het een parameter is van de
functie.
Als een functie wordt aangeroepen, moet je een waarde geven aan iedere
(verplichte) parameter die voor de functie gedefinieerd is. Zo’n waarde
wordt een “argument” genoemd. Dus, om de functie hallo()
aan te
roepen, moet een argument waarde voor de parameter naam
gespecificeerd
worden. Je plaatst een argument tussen de haakjes van de functie
aanroep. Merk op dat het niet nodig is dat je in het hoofdprogramma weet
dat de parameter naam
wordt genoemd in de functie. Hoe hij heet is
niet belangrijk. Het enige wat je moet weten is dat er een parameter is
die een waarde nodig heeft, en liefst ook de beperkingen die de functie
oplegt aan de waarde (bijvoorbeeld het type parameter dat de schrijver
van de functie wilt dat je meegeeft).
def hallo( naam ):
print( "Hallo," naam +"+")
hallo( "Groucho" )
hallo( "Chico" )
hallo( "Harpo" )
hallo( "Zeppo" )
Parameters van een functie zijn niet meer en niet minder dan variabelen die je kunt gebruiken in de functie, en die hun (initiële) waarde krijgen van buiten de functie (namelijk via de functie aanroep). De parameters zijn “lokaal” voor de functie, dat wil zeggen, ze kunnen niet benaderd worden door code die niet in het blok code van de functie staat, noch kunnen ze waardes van variabelen buiten de functie beïnvloeden. Gebruik nooit dezelfde variabelen in je hoofdprogramma als in een functie om verwarring te vermijden.
Functies kunnen meerdere parameters hebben. Bijvoorbeeld, de volgende functie krijgt twee parameters en vermenigvuldigt hun waardes, waarna het resultaat wordt afgedrukt:
def vermenigvuldig( x, y ):
resultaat = x * y
print( resultaat )
vermenigvuldig( 2020, 5278238 )
vermenigvuldig( 2, 3 )
Je kunt default waardes geven aan sommige parameters. Je doet dit door
bij het specificeren van de parameter een assignment operator (=
) en
de gewenste waarde op te nemen, alsof het een reguliere assignment is.
Als je de functie aanroept, mag je een waarde voor alle parameters
opgeven, of slechts voor een aantal. De waardes worden aan de parameters
gegeven van links naar rechts. Als je minder waardes opgeeft dan er
parameters zijn, maar er default waardes gegeven zijn aan de parameters
waarvoor je niks opgeeft, krijg je geen runtime fouten. Als je een
functie definieert met default waardes voor een aantal parameters, maar
niet voor alle parameters, dan is het de gewoonte om de parameters
waarvoor je een default waarde opgeeft rechts te plaatsen van de
parameters waarvoor je geen default waarde hebt.
Als je de default waarde van een specifieke parameter wilt vervangen, en
je kent de naam van de parameter maar je weet niet waar hij staat in de
patameter-orde, of je wilt simpelweg de default waardes van de overige
parameters intact laten, dan kun je in de aanroep van de functie deze
specifieke parameter overschrijven door een assignment aan de
parameter op te nemen tussen de haakjes, dus
<functie>( <parameternaam>=<waarde> )
.
De volgende code geeft voorbeelden van deze mogelijkheden:
def vermenigvuldig_xyz( x, y=1, z=7 ):
print( x * y * z )
vermenigvuldig_xyz( 2, 2, 2 ) # x=2, y=2, z=2
vermenigvuldig_xyz( 2, 5 ) # x=2, y=5, z=7
vermenigvuldig_xyz( 2, z=5 ) # x=2, y=1, z=5
return
Parameters worden gebruikt om informatie van buiten de functie naar de
functie toe te communiceren. Vaak wil je ook informatie vanuit de
functie naar het programma buiten de functie toe communiceren. Daartoe
dient het commando return
.
Als Python return
tegenkomt in een functie, beëindigt dat de functie.
Python gaat dan verder met de code vlak na de plek waar de functie werd
aangeroepen. Je mag achter het woord return
nul, één, of meerdere
waardes of variabelen opnemen. Deze waardes worden dan gecommuniceerd
naar het programma buiten de functie. Als je de waardes wilt gebruiken,
moet je zorgen dat ze in een variabele terecht komen. Dat krijg je voor
elkaar door de functie-aanroep te doen als een assignment naar een
variable.
Bijvoorbeeld, stel dat een functie wordt aangeroepen als
<variabele> = <functie>()
. De functie maakt een berekening, die wordt
opgeslagen in een <resultaat_variabele>
. Het commando
return <resultaat_variabele>
beëindigt de functie, waarna de waarde
die in <resultaat_variabele>
staat “uit de functie komt.” Omdat
<functie>()
was aangeroepen via een assignment, komt de waarde van
<resultaat_variabele>
uiteindelijk terecht in <variabele>
.
Ik snap dat dit wat ingewikkeld klinkt, maar het wordt waarschijnlijk duidelijk als je het volgende voorbeeld bestudeert:
from math import sqrt
def pythagoras( a, b ):
return sqrt( a*a + b*b )
c = pythagoras( 3, 4 )
print( c )
De functie pythagoras()
berekent de wortel van de som van de kwadraten
van de parameters. De uitkomst wordt via een return
statement
geretourneerd. Het hoofdprogramma “vangt” de waarde door het toekennen
van de waarde aan de variabele c
. Daarna wordt de inhoud van c
geprint.
return
en print
In het verleden heb ik diverse malen geconstateerd dat veel studenten moeite hebben met het verschil tusen een functie die een waarde retourneert, en een functie die een waarde print. Vergelijk de volgende twee stukken code:
def print3():
print( 3 )
print("na 3 komt 4")
print3()
en:
def return3():
return 3
print("komt 4 na 3?")
print( return3() )
Zowel de functie print3()
als de functie return3()
worden
aangeroepen in hun respectievelijke hoofdprogramma’s, en beide
resulteren in het printen van de waarde 3. Het verschil is dat bij
print3()
dit printen gebeurt in de functie en de functie niks
retourneert, terwijl bij de functie return3()
de waarde 3 wordt
geretourneerd, en geprint in het hoofdprogramma. Voor de gebruiker lijkt
er geen verschil te zijn: beide programma’s printen 3. Voor een
programmeur zijn beide functies echter compleet verschillend.
Merk ook op dat bij print3()
de tekst "na 3 komt 4"
ook wordt getoond.
Wordt de tekst "komt 4 na 3?"
ook getoond bij return3()
?
De functie print3()
kan voor slechts één doel gebruikt worden,
namelijk het tonen van het getal 3. De functie return3()
kan echter
gebruikt worden waar je ook maar het getal 3 nodig hebt: om het te
tonen, om het te gebruiken in een berekening, of om het aan een
variabele toe te kennen. Bijvoorbeeld, de volgende code verheft 2 tot de
macht 3 en print de uitkomst:
def return3():
return 3
x = 2 ** return3()
print( x )
De code hieronder, echter, geeft een runtime error:
def print3():
print( 3 )
x = 2 ** print3() # Runtime error!
print( x )
De reden is dat hoewel print3()
het getal 3 op het scherm toont (je
ziet het boven de runtime error als je de code uitvoert), het niet de
waarde 3 produceert op een manier dat een berekening er gebruik van kan
maken. De functie print3()
geeft de speciale waarde None
, en die kan
niet in een berekening gebruikt worden.
Dus als je een functie wilt maken die een waarde oplevert die elders in
je code gebruikt moet worden, dan moet die functie de waarde middels een
return
retourneren. Als je een functie wilt maken die informatie
toont, dan kun je dat gewoon doen middels print statements in de
functie, en hoeft de functie niets te retourneren.
Als je nog steeds moeite hebt met begrijpen hoe functies werken, denk dan als volgt:
Een functie is net een grote machine, bijvoorbeeld, een machine die pannenkoeken bakt. Er zijn input sleuven aan de bovenkant, met labels “melk,” “eieren,” en “meel.” Dat zijn de functie parameters. Je kunt beïnvloeden wat voor pannenkoeken de machine bakt door de juiste ingrediënten in de inputs te stoppen; bijvoorbeeld, als je volkoren pannenkoeken wilt, doe je volkoren meel in de “meel” input. Natuurlijk kunnen de zaken dramatisch fout lopen als je eieren in de “melk” input doet – of als je probeert een kat in de “meel” input te stoppen.
Als de inputs gevuld zijn, begint de machine te ratelen. Je wacht geduldig naast de output sleuf die (tot niemand’s verrassing) het label “return” draagt. Na enige tijd verschijnt er een pannenkoek in de uitvoer. Die pannenkoek is de retourwaarde die de machine heeft geproduceerd.
De machine heeft ook een display. Als je iets incorrects in de machine stopt, komt daar wellicht een boodschap op in de trant van “Kat zit vast in machine, herstarten alsjeblieft.” Dit display is waar alles dat in de machine geprint wordt verschijnt.
Je snapt wel dat het zinloos is als, nadat je de juiste ingrediënten geladen hebt, de machine alleen maar op de display toont “Pannenkoek gereed!” Je wilt een echte pannenkoek. Daarom moet de machine de pannenkoek aanreiken via de “return,” waarvandaan jij hem kunt aannemen en “assignen” aan je lunch. Een tekst alleen is onvoldoende.
Een van de aardige dingen van de pannenkoek machine is dat zelfs als jij niet weet hoe je een pannenkoek moet bakken, je toch pannenkoeken kunt krijgen als je maar de juiste ingrediënten verstrekt. Dat is ook aardig aan functies: ze kunnen complexe zaken voor je regelen, zonder dat je hoeft te weten hoe ze dat doen.
Je bent niet beperkt in je functies tot slechts één retourwaarde. Je kunt meerdere waardes in één keer retourneren door er komma’s tussen te zetten. Als je deze waardes wilt gebruiken in je programma na aanroep van de functies, moet je ze toekennen aan meerdere variabelen. Die zet je allemaal aan de linkerkant van de assignment operator, ook met komma’s ertussen. Dit kan ik het beste illustreren aan de hand van een voorbeeld:
import datetime
def plusDagen( jaar, maand, dag, increment ):
startdatum = datetime.datetime( jaar, maand, dag )
einddatum = startdatum + datetime.timedelta( days=increment )
return einddatum.year, einddatum.month, einddatum.day
y, m, d = plusDagen( 2015, 11, 13, 55 )
print(f'{y}/{m}/{d}')
De functie plusDagen()
krijgt vier argumenten, namelijk integers die
een jaar, een maand, een dag, en een aantal dagen die je wilt optellen
bij de datum die wordt weergegeven door de eerste drie argumenten. Er
worden drie waardes geretourneerd, namelijk een nieuw jaar, een nieuwe
maand, en een nieuwe dag. De code stopt die in de variabelen y
, m
,
en d
.
Als je de code hierboven bestudeert, vraag je je misschien af hoe
plusDagen()
precies werkt. Zoals ik al zei, het is een voordeel van
functies dat zolang ze hun werk doen en je weet wat de argumenten zijn
en wat er geretourneerd wordt, dat je niet hoeft te weten hoe de functie
in elkaar zit. Je kunt de functie gebruiken zonder kennis van het
interne proces. Dus je mag gewoon negeren wat je in plusDagen()
ziet
staan (overigens gebruikt de code van plusDagen()
de datetime
module, die aan de orde komt in hoofdstuk
282).
Functies mogen andere functies aanroepen, zolang die andere functies
maar bekend zijn bij de aanroepende functie. Bijvoorbeeld, hieronder zie
je hoe de functie afstand()
de functie pythagoras()
gebruikt om de
afstand te berekenen tussen twee punten in een 2-dimensionale ruimte.
from math import sqrt
def pythagoras( a, b ):
if a <= 0 or b <= 0:
return -1
return sqrt( a*a + b*b )
def afstand( x1, y1, x2, y2 ):
return pythagoras( abs( x1 - x2 ), abs( y1 - y2 ) )
print( afstand( 1, 1, 4, 5 ) )
afstand()
kent pythagoras()
, omdat pythagoras()
gedefinieerd is
vóór afstand()
is aangeroepen.
Als je wilt, kun je functies in andere functies stoppen; met andere
woorden, je kunt functies “nesten.” Bijvoorbeeld, je kunt de functie
pythagoras()
in de functie afstand()
stoppen. Dat betekent dat
pythagoras()
kan worden aanroepen vanuit afstand()
, maar niet op
andere plekken in de code.
from math import sqrt
def afstand( x1, y1, x2, y2 ):
def pythagoras_binnen( a, b ):
if a <= 0 or b <= 0:
return -1
return sqrt( a*a + b*b )
return pythagoras_binnen( abs( x1 - x2 ), abs( y1 - y2 ) )
print( afstand( 1, 1, 4, 5 ) )
# print( pythagoras_binnen( 3, 4 ) )
Het gebruik van geneste functies is wat uitzonderlijk, maar het is mogelijk.
Merk op: Als je de hash mark verwijdert voor de laatste regel in de code
hierboven, wordt er een aanroep van pythagoras_binnen()
toegevoegd
aan de code. Bij uitvoering van het programma geeft dat een runtime
error, omdat pythagoras_binnen()
alleen zichtbaar is in de functie
afstand()
.
Schrijf een functie printx()
die alleen de letter “x” print. Schrijf
daarna een functie meerderex()
die als argument een integer krijgt en
die zo vaak de letter “x” print als de integer aangeeft. Daartoe roept
de functie meerderex()
zo vaak als nodig de functie printx()
aan.
Het is de gewoonte om namen van functies niet te laten beginnen met een underscore (zulke functienamen zijn voorbehouden aan de ontwikkelaars van Python zelf), en dat je probeert alleen kleine letters te gebruiken. Als een functienaam uit meerdere woorden bestaat, kun je ofwel underscores tussen die woorden zetten, ofwel ieder woord behalve het eerste laten starten met een hoofdletter. Verschillende programmeurs hebben hier verschillende meningen over, maar in de praktijk maakt het niet zoveel uit omdat je een functie altijd kunt herkennen aan het feit dat er haakjes achter de functienaam staan.
Bepaalde benamingen van functies zijn typerend voor bepaalde functionaliteiten.
Een functie die test of een bepaald item een bepaalde eigenschap heeft,
en die dan True
of False
retourneert afhankelijk van het feit of de
eigenschap wel of niet aanwezig is, krijgt meestal een naam die begint
met het woord is
, gevolgd door de naam van de eigenschap die start met
een hoofdletter. Bijvoorbeeld, een functie die test of een getal even
is, zou de naam isEven()
krijgen.
Schrijf de functie isEven()
.
Schrijf de functie isOneven()
, die bepaalt of een getal oneven is,
door de functie isEven()
aan te roepen en het resultaat te inverteren.
Merk op: Als je een functie als isEven()
wilt gebruiken in een
conditionele expressie, bijvoorbeeld, als je een actie alleen wilt
uitvoeren als een getal even is, hoef je niet te schrijven
if isEven( num ) == True:
. Je hoeft alleen maar te schrijven
if isEven( num ):
, want de functie retourneert al True
of False
.
Een is-functie op zo’n manier gebruiken verhoogt de leesbaarheid van een
programma.
De naam van een functie die de waarde van een attribuut opvraagt en
retourneert, begint over het algemeen met het woord get
, gevolgd door
de naam van de eigenschap, beginnend met een hoofdletter. Bijvoorbeeld,
een functie die van een float alleen het fractionele gedeelte (de
cijfers achter de komma) retourneert, zou heten getFractie()
.13
De tegenhanger van een “get” functie is een functie die een eigenschap
een bepaalde waarde geeft. De naam van zo’n functie begint meestal met
set
, en is voor de rest gelijk aan een get
functie. Ik kan op dit
punt geen voorbeeld van een set
functie geven, aangezien ik nog niet
heb uitgelegd hoe een functie een waarde aan iets kan geven, aangezien
functies de waardes van de variabelen die als argumenten zijn meegegeven
niet kunnen wijzigen (tenminste, niet voor de data types die tot nu toe
zijn geïntroduceerd). Dit volgt in een later hoofdstuk.
Als je je aan dergelijke functiebenamingen houdt, wordt je code beter leesbaar.
In alle hoofdstukken tot nu toe heb ik weinig commentaar in code geschreven. Boeken en cursussen die programmeren doceren moedigen studenten vaak aan om commentaar aan code toe te voegen. Ikzelf ben van mening dat code “zelf-documenterend” moet zijn, dat wil zeggen, dat je gemakkelijk aan code kunt zien wat het doet door het te lezen. Je kunt dat bereiken door goede namen te kiezen voor variabelen en functies, door gebruik te maken van spaties en witregels, door nette inspringing (wat gelukkig bij Python een noodzaak is), en door geen gebruik te maken van “slimme” truukjes die je code een beetje sneller maken ten koste van leesbaarheid, enkel en alleen om te laten zien hoe slim je bent.24
Hoewel ik commentaar zie als iets extra’s dat je alleen moet gebruiken als er een noodzaak is om je code uit te leggen, is mijn opinie over commentaar bij functies anders. Het idee achter een functie is dat de gebruiker van de functie de code van de functie niet hoeft te kennen. Daarom moet hetgeen de functie doet en hoe de functie werkt worden uitgelegd middels commentaar, geschreven meteen boven de functienaam.
In commentaar bij een functie moet je drie zaken uitleggen:
Wat de functie doet
Welke argumenten de functie nodig heeft of accepteert, inclusief data types
Wat de functie retourneert, inclusief data types
Als een functie neveneffecten heeft, dus als een functie zaken buiten de functie beïnvloedt, dan moet dat ook heel duidelijk in het functiecommentaar staan. Ik heb dat niet in het lijstje hierboven genoemd, omdat een functie geen neveneffecten zou mogen hebben.
Voor de antwoorden bij de opgaves in dit hoofdstuk heb ik commentaar aan de functies toegevoegd op een manier die ik acceptabel acht. In volgende hoofdstukken doe ik dat niet altijd, omdat ik de functies vaak in de tekst bediscussieer, of omdat ik vind dat je de inhoud van een functie moet bestuderen. Maar ik schrijf wel altijd commentaar bij functies die ik voor andere doeleinden gebruik.
“Get” is uiteraard de Engelse term voor “nemen.” Je mag hier in het Nederlands dan ook het woord “neem” voor gebruiken in plaats van “get.” ↩5
Een kleine anecdote wat dit betreft: tijdens mijn dagen als professioneel programmeur hoorde ik een collega eens een andere collega ophemelen door te zeggen: “Hij is zo briljant, als ik zijn code zie snap ik er niks van!” Als iemand dat over mijn code zou zeggen zou ik me diep schamen. ↩6