TOPIC 3: Functies


In het vorige topic hebben we o.a. geleerd hoe we reeds bestaande functies kunnen oproepen in Python. De syntaxregels schrijven voor dat je de argumenten tussen haakjes moet zetten en dient te scheiden door komma’s.

De makers van Python konden wél voorzien dat we soms dingen als abs nodig hebben. Maar ze konden bijvoorbeeld niet voorzien dat wij nu toevallig ooit eens de functie \(f(x) = 10x^2-17x+8\) nodig hebben. Daarom laat Python toe dat we zélf functies maken. In dit topic leren we hoe we het arsenaal ingebouwde functies in Python kunnen uitbreiden met zelfgemaakte functies. De functies uit math en matplotlib die we moesten importeren in het vorige topic zijn eigenlijk ook “zelf” gemaakte functies. De auteurs van math hebben deze zelfgemaakte functies in modules gestopt die bij iedere Python installatie standaard zijn meegeleverd, en die wij dan hebben kunnen (her)gebruiken. Ook de functies uit de matplotlib module werden ooit door haar auteurs “zélf” gemaakt. In dit topic leren we dus hoe ook wij zulke modules met zelfgemaakte functies kunnen maken.

We beginnen met het definiëren van eenvoudige wiskundige functies zoals de \(f\) van hierboven. Al gauw zullen we nood hebben aan meer ingewikkelde functies die hun rekenwerk baseren op bepaalde beslissingen die door de functie genomen moeten worden. Deze beslissingen worden door Python genomen met behulp van het if statement. De voorwaarde waarop zo’n beslissing gebaseerd is kan waar of vals zijn, wat meteen een goede motivatie is om het Booleaanse type uit te leggen (met waarden True en False). We eindigen het topic met het programmeren van ingewikkeldere functies die tupels als argumenten en resultaten opleveren.

Maar eerst dus gewone wiskundige functies.

Functies definiëren in Python


Inleiding

Beschouw volgende wiskundige functie \(f\):

\[f(t) = \frac{5}{9} . ( t - 32)\]

Deze functie converteert een temperatuur gegeven in graden Fahrenheit naar het equivalent ervan in graden Celsius. Indien we de functie toepassen op \(80\) — genoteerd \(f(80)\) — krijgen we het resultaat \(26,66666...\).

We kunnen deze functie in Python als volgt definiëren en vervolgens oproepen:

def to_celsius(t):
    return (t - 32.0) * 5.0 / 9.0
to_celsius(80)
26.666666666666668

Laat ons de eerste codecel even analyseren. Ze bevat een functiedefinitie. Iedere functiedefinitie bestaat uit het woordje def, gevolgd door de naam van de functie, gevolgd door de parameters van de functie (in dit geval is dat slechts één parameter t), gevolgd door een dubbele punt, gevolgd door de body van de functie. De body is wat we in de wiskunde het functievoorschrift zouden noemen. Het bevat de Python code die uitrekent wat er uit de functie dient te komen. In de body van deze functie zien we slechts één statement staan: het return statement. Het return statement doet Python terugkeren naar de oproeper met de waarde van de opgegeven expressie. In ons voorbeeld is to_celsius de naam van de functie.

In de er op volgende codecel zien we de oproep van onze versgebouwde functie met 80 als argument. Een zélfgebouwde functie wordt dus syntactisch helemaal op dezelfde manier opgeroepen als de ingebouwde functies uit het vorige topic. Maar hier is wat er gebeurt bij de uitvoering: Python “springt” bij de oproep van de functie naar de body. Hierbij worden de parameters van de functie (in dit geval dus t) tijdelijk geassocieerd met de argumenten van de oproep (in dit geval 80). Dan wordt de body geëvalueerd. In ons geval staat daar een return statement. Dat doet Python de expressie evalueren en met de waarde “terugspringen” naar de oproeper. In ons geval is dat de REPL. Deze zal de resulterende waarde zoals gewoonlijk op het scherm printen.

Terminologie

Omtrent het oproepen van zelfgemaakte functies is er in de computerwetenschappen over de jaren heen (helaas) wat terminologische verwarring ontstaan. Ook wij zullen beide soms door elkaar gebruiken:

  • Soms spreekt men van parameter en argument. De parameter is de naam van het argument in de definitie van de functie. In ons voorbeeld is t de parameter en is 80 het argument.

  • Soms spreekt men echter van de formele parameter(s) en de actuele parameter(s). Dit is gewoon andere terminologie om hetzelfde aan te duiden. Indien we deze terminologie gebruiken, is t dus de formele parameter en is 80 de actuele parameter.

Hieronder tonen we nog een voorbeeld van een zelfgemaakte functie. Deze functie heeft opnieuw één parameter en berekent het kwadraat van het meegegeven argument.

def square(x):
    return x*x
square(4)
16
square(4.0)
16.0
square(3)+square(7)
58
square(square(3))
81

Interessant in deze experimenten is de lijn square(square(3)). Deze zal onze functie oproepen met 3. Dat zal uiteraard \(9\) opleveren. Maar vermits daar nog eens een oproep van square omheen staat, zal de functie nogmaals worden opgeroepen met \(9\). Het resultaat is daarom \(81\).

Het volgende voorbeeld toont dat we ook functies van meerdere parameters kunnen maken. average neemt twee argumenten (genaamd x en y) en berekent er het rekenkundig gemiddelde van. Bij zulke functies van meerdere argumenten dienen zowel de parameters (bij de definitie) als de argumenten (bij de oproep) gescheiden te worden door komma’s.

def average(x,y):
    return (x + y) / 2.0
average(1,2)
1.5

In principe moet het aantal argumenten en het aantal parameters kloppen. Onderstaande experimenten laten zien wat gebeurt als je te veel of te weinig parameters meegeeft. We zien een beetje verderop dat je wel optionele parameters kan introduceren.

average(1,2,3)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-10-51c4f85249a8> in <module>
----> 1 average(1,2,3)

TypeError: average() takes 2 positional arguments but 3 were given
average(1)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-11-0c1aa5f6a438> in <module>
----> 1 average(1)

TypeError: average() missing 1 required positional argument: 'y'

Merk tenslotte op dat return expliciet geschreven dient te worden teneinde met een betekenisvolle waarde uit een functie terug te keren. Hetvolgende laat zien wat er gebeurt als we return vergeten.

def square_met_return_vergeten(x):
    x*x
square_met_return_vergeten(4)

We zien dus dat de functie netjes wordt opgeroepen (er komt immers geen foutmelding) en dat de body dus wordt uitgevoerd. Op het einde van de body keert Python gewoon terug naar de oproeper (in dit geval de REPL). Op dit moment lijkt het alsof er geen waarde werd teruggegeven. Dat is echter niet helemaal correct. Bij het ontbreken van een return statement zal Python -op het einde van een functie-body- automatisch met de waarde None terugkeren naar de oproeper. Het speciale aan None is dat de printfase er niets mee doet. Maar het is wel degelijk een waarde als een andere. Het is de enige waarde van het type NoneType dat speciaal is de taal is voorzien om met “niets” om te gaan. Meer hierover later.

Parameters zijn Lokaal

Het volgende experiment illustreert een heel belangrijke eigenschap van Python functies, namelijk dat parameters lokaal aan de functie zijn. Hiermee bedoelen we dat de namen van de parameters enkel gebruikt mogen worden binnenin de body van de functie en niet daarbuiten. Dat zien we in het volgende experiment. square oproepen werkt perfect, daarbinnen is de x dus gekend. Maar gewoon proberen x te evalueren.levert een foutmelding op aangezien. x is in de REPL niet gedefinieerd. De parameter x is dus lokaal aan de functie square.

def square(x):
    return x * x
square(3)
9
x
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-16-6fcf9dfbd479> in <module>
----> 1 x

NameError: name 'x' is not defined

Het volgende stukje code toont een tweede belangrijke eigenschap van Python functies en dat is namelijk dat de precieze naam van een parameter niet terzake doet voor de gebruiker (t.t.z. de oproeper). Stel dat square zou gedefiniëerd zijn met een parameter y en niet met een x zoals hiervoor. Voor de oproeper verandert er niets. De keuze van de namen van parameters is dus enkel de zaak van degene die de functie schrijft, niet van de oproeper.

def square(y):
    return y * y
square(3)
9

Uiteraard dienen we consistent te zijn in de definitie van de parameters en het gebruik ervan binnenin de body van de functie. Indien we kiezen voor y als parameter maar we gebruiken vervolgens x in de body, dan krijgen we een foutmelding tijdens de uitvoer van de body: Python klaagt dat x niet gedefinieerd is in die functie.

def square(y):
    return x * x
square(3)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-20-2cfd8bba3a88> in <module>
----> 1 square(3)

<ipython-input-19-df3a164a104b> in square(y)
      1 def square(y):
----> 2     return x * x

NameError: name 'x' is not defined

Merk op dat dit de eerste keer is dat we een foutmelding zien die uit 2 stukken bestaat. Dat komt omdat Python onthoudt telkens wanneer een functie wordt opgeroepen. Indien je dus een functie oproept die op haar beurt een functie oproept die dan een fout oplevert, zal je een foutmelding uit 3 stukken zien. Door de foutmelding correct te lezen kan je dus te weten komen waar het is misgelopen. In bovenstaande foutmelding zegt Python dat de fout ontstond in de naamloze “<module>” (t.t.z. de REPL). In het tweede stuk wordt er gezegd dat de fout (meer precies dus) ontstond in de functie square.

Naamgeving

In tegenstelling tot wiskundigen geven computerwetenschappers meestal een goed gekozen naam aan hun functies en variabelen. De bedoeling hiervan is dat je later nog wijs geraakt uit je eigen code. Dat geeft dan aanleiding tot een stijl van programmeren die heel persoonlijk is. Zo is onderstaande functie gemiddelde perfect geldige Python en is het technisch-wetenschappelijk volledig equivalent met bovenstaande definitie van average.

def gemiddelde(eerste_getal, een_zeer_rare_naam2):
    return (eerste_getal + een_zeer_rare_naam2) / 2.0
gemiddelde(1,2)
1.5

Belangrijk is dat je “goede” namen kiest zodat je later je eigen code nog kan lezen. Kies dus liever average, celsius of final_result i.p.v. weinig zeggende namen zoals x1, x2 of blah. Verder raden we aan van zoveel mogelijk consistent te zijn in je keuze. Kies dus niet max_val in één functie en maxVal of maxval of maximum_val in een andere. Inconsistente naamgeving maakt code doorgaans moeilijk leesbaar en verhoogt het aantal vergissingen bij het progranmmeren. Maar nogmaals, over smaak en kleur valt niet te twisten. Dit geldt zowel voor variabelen, functies als parameters van functies.

Procedurele Abstractie

Hieronder zien we de functie sum_of_squares die de som van de kwadraten van twee getallen x en y berekent.

def sum_of_squares(x,y):
    return x**2 + y**2
sum_of_squares(2,3)
13

Alhoewel deze functie helemaal correct is, zullen de meeste computerwetenschappers echter volgende variante van sum_of_squares verkiezen. Door de idee “kwadrateren” in een aparte Python functie te stoppen wordt onze code plots veel meer leesbaar.

def square(x):
    return x**2

def sum_of_squares(x,y):
    return square(x)+square(y)
sum_of_squares(2,3)
13

We stoppen dus zoveel mogelijk “logisch samenhorende berekeningen” in één functie die we vervolgens een goed gekozen naam geven. Dit heet procedurele abstractie. Door een expressie als functievoorschrift in een functie met een goede naam te stoppen abstraheren we de complexiteit van die expressie “weg” omdat we er vanaf dan abstract over kunnen nadenken. Indien we bijvoorbeeld een gegeven meetwaarde in graden Fahrenheit wensen te converteren dienen we gewoon to_celsius op te roepen zonder dat we ons zorgen dienen te maken over hoe dat converteren nu weeral precies in zijn werk ging.

Procedurele abstractie heeft als grote voordeel dat we complexiteit wegstoppen achter betekenisvolle namen en dat onze Python code hierdoor veel leesbaarder wordt. Procedurele abstractie heeft nog een tweede belangrijk voordeel. Wanneer we bijvoorbeeld een gegeven expressie twee of meerdere keren nodig hebben, kunnen we deze expressie beter wegstoppen in een functie. We dienen de expressie dan maar één keer te schrijven! Dat heeft als belangrijkste voordeel dat we latere veranderingen (bijvoorbeeld bij het ontdekken van een vergissing) slechts één keer hoeven te doen en dat alle code die de functie oproept hiermee automatisch aangepast is.

Lokale Variabelen & Blocks

Tot nu toe bestonden de body’s van onze functies slechts uit één enkel return statement. Dat is meestal echter niet het geval. In het algemene geval bestaat de body van een functie uit een block. Een block bestaat uit verschillende statements die door Python één na één worden uitgevoerd. We kunnen een block herkennen aan het feit dat alle statements van het block op hetzelfde niveau (van “de kantlijn”) worden ingesprongen. Hieronder zien we een voorbeeld.

def polynomial(a, b, c, x):
    first  = a * x * x
    second = b * x
    third  = c
    return first + second + third
polynomial(2, 3, 4, 0.5)
6.0
polynomial(2, 3, 4, 1.3)
11.280000000000001

De functie polynonial verwacht 4 argumenten. Haar body is een block dat uit 4 statements[^2] bestaat: 3 assignment statements en daarna nog een return statement. In het algemeen worden de statements uit een block één na één, van boven naar onder, uitgevoerd. Python gaat dus bij het oproepen van de functie eerst de variabele first definiëren, dan de variabele second, daarna de variabele third en zal ten slotte de return uitvoeren (en dus met de som van de drie eerste naar de oproeper terugkeren).

Het is uiterst belangrijk te begrijpen dat de toekenningen die binnenin een functie worden uitgevoerd aanleiding geven tot variabelen die lokaal zijn voor die functie. Deze lokale variabelen bestaan dus enkel tijdens de uitvoering van de body van de functie en worden na de oproep opgeruimd door Python. Dus net zoals parameters lokaal zijn aan een functie zijn de variabelen die worden geïntroduceerd binnen een functie lokaal aaan de functie. Dat zien we als we onderstaande code meteen na de voorgaande code uitvoeren:

a
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-30-3f786850e387> in <module>
----> 1 a

NameError: name 'a' is not defined
first
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-31-271ac93c44ac> in <module>
----> 1 first

NameError: name 'first' is not defined

Algemeen ziet de syntax voor het definiëren van functies er dus als volgt uit.

Bij de oproep van de functie worden alle statements van het blok één na één uitgevoerd. Indien één van die statements het return statement is, wordt er meteen uit de functie gesprongen met de bijhorende waarde. Indien geen enkel statement uit het block return is, “valt” de functie terug naar de oproeper met de waarde None.

Optionele Parameters

In onderstaande code is t een functie die 2 parameters heeft waarvan er één optioneel is (namelijk g). Dat zien we aan het feit dat de parameter g reeds in de definitie van de functie een waarde krijgt (in dit geval \(9.81\)). Men noemt dit de default waarde van g. Optionele parameters kunnen weggelaten worden bij de oproep. In dat geval wordt de default waarde gebruikt.

from math import sqrt

def t(h, g = 9.81):
    return sqrt(2*h / g)
t(1)
0.4515236409857309

Indien we echter bij de oproep alsnog een tweede argument meegeven dan wordt déze waarde gebruikt en wordt de default waarde genegeerd. Dit wordt aangetoond in volgend experiment. Deze oproep maakt wél gebruik van de mogelijkheid om een tweede argument mee te geven (in dit geval 1.63).

t(1, 1.63)
1.1076975512434226

De oproep hieronder laat tenslotte nog een nieuwigheid zien. Tot nu toe hebben we bij de oproep van functies de naam van de parameter niet vermeld. Dat is immers een interne zaak van die functie! De derde oproep laat zien dat we toch bij de oproep ook de naam van de parameter mogen vermelden (hier: g). Eigenlijk is dit een slechte gewoonte. Het zorgt er namelijk voor dat degene die de functie ooit schreef nooit van gedacht mag veranderen over de naam van zijn parameters. Immers, alle oproepers die die naam expliciet vermelden zullen dan niet meer werken.

t(1, g = 1.63)
1.1076975512434226

Toch wordt deze gewoonte zeer veel gebruikt in Python code. Vooral bij functies die veel verschillende opties hebben kan je dit tegenkomen zoals bv. de plot functie uit Matplotlib. Stel dat je een functie hebt met 12 argumenten. Ofwel moet je als programmeur de volgorde van de 12 argumenten onthouden, ofwel gebruikt je de naam van de parameter expliciet. Het laatste kan soms makkelijker zijn vooral als er goede parameternamen gekozen zijn.

Booleaanse Waarden


De functies die we tot nu toe hebben gemaakt zijn tamelijk saai. Het zijn eigenlijk allemaal gewoon wiskundige “formules” die in één return statement zijn ondergebracht of onder te brengen zijn. We gaan nu ons arsenaal aan Python statements en expressies uitbreiden om interessantere functies te kunnen schrijven. Eén van die statements is het if statement. Maar alvorens we dat kunnen uitleggen hebben we nog snel een nieuw type nodig: bool.

Waarheidswaarden

Het type bool werd vernoemd naar de wiskundige George Boole. Het kent slechts 2 waarden: True en False. Het is belangrijk deze met een hoofdletter te schrijven. Anders denkt Python dat we de naam van een (ongedefinieerde) variabele gebruiken. Net zoals getallen evalueren de Booleaanse waarden naar zichzelf.

True
True
true
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-37-724ba28f4a9a> in <module>
----> 1 true

NameError: name 'true' is not defined

Booleaanse Operatoren

Net zoals voor int en float, bestaan er voor bool een aantal ingebouwde operatoren. Deze zogenoemde booleaanse operatoren implementeren de beroemde waarheidstabellen uit de logica. Python kent de operatoren not, and en or. Toepassen van de operator not op een booleaanse waarde levert de andere booleaanse waarde op:

not True
False
not False
True

Toepassen van de operator and op twee booleaanse waarden geeft True als en slechts als beide operanden ook True waren:

True and True
True
True and False
False
False and True
False
False and False
False

Toepassen van de operator or op twee booleaanse waarden ten slotte, geeft True van zodra één van beide operanden True is:

True or True
True
True or False
True
False or True
True
False or False
False

Voorrangsregels

Net zoals er voorrangsregels bestaan tussen + en *, zijn ook and en or aan zulke voorrangsregels onderworpen. Uit hetvolgende experiment kan je makkelijk afleiden welke operator voorrang heeft. Doe het!

windy = False
cold  = False
sunny = True
windy and cold or sunny
True

Relationele Operatoren

Alhoewel bool op zichzelf als verzameling best wel interessant is (men spreekt over booleaanse algebra), komt het type toch pas goed tot zijn recht indien we het in verband brengen met waarden van andere types. Dat doen de zogenoemde relationele operatoren. Dat zijn operatoren die een relatie tussen twee waarden aftoetsen en vervolgens True of False opleveren afhankelijk van het feit of de relatie waar is of niet. Ziehier de werking van de 5 relaties >, <, ==, <= en >=. Merk op dat de gelijkheidsrelatie (men spreekt in de computerwetenschappen eerder over een gelijkheidstest) met een dubbele == wordt genoteerd. De enkele = is in Python immers reeds gereserveerd voor de toekenning. Merk ten slotte nog op dat != de Python versie is van wat wiskundigen als \(\neq\) aanduiden.

45 > 34
True
45 > 79
False
45 < 79
True
45 < 34
False
45 == 34
False
10 <= 100
True
100 >= 200
False

Zoals we in het volgende experiment zien, zijn de relationele operatoren stevig overladen. Men kan ze op haast alle types toepassen. Doorgaans moeten beide operanden van hetzelfde type zijn om de test te kunnen evalueren. Bij int en float is dat dan weer niet het geval. En alhoewel de float 67.0 en de int 67niet hetzelfde getal zijn wordt ze toch als “van gelijke waarde” gezien door de == operator.

23.1 >= 23
True
23.1 >= 23.1
True
23.1 <= 23
False
67.0 == 67
True

Voor het vergelijken van strings wordt de zogenoemde lexicografische orde gebruikt. Dat is de orde die gebruikt wordt door woordenboekenschrijvers. Hieronder zien we enkele experimenten om het principe te illustreren:

'A' < 'a'
True
'A' > 'z'
False
'abc' < 'abd'
True
'abc' < 'abcd'
True
'hallo' == 'HalLo'
False
'hallo' == 'hallo'
True
'hallo' != 'hallo'
False

Predikaten

Relationele operatoren kunnen gebruikt worden in de body van functies bijvoorbeeld om te bepalen een zekere eigenschap over de meegegeven argumenten waar of vals is. Zulke functies die een booleaanse waarde weergeven noemen we predikaten. Ze “prediken” als het ware iets over de argumenten die aan de functie worden meegegeven. Hieronder zien we hoe we een predicaat positive kunnen schrijven dat True of False weergeeft afhankelijk van het feit of het argument een positief getal is. De expressie in het return statement past de operator >= toe en levert dus na evaluatie een booleaanse waarde op die meteen ook het resultaat is van het predikaat.

def positive(x):
    return x >= 0
positive(3)
True
positive(-2)
False
positive(0)
True

Hieronder zien we nog een predikaat dat gebruik maakt van een iets ingewikkeldere booleaanse expressie. is_sin bepaalt of een gegeven getal al dan niet een geldige sinuswaarde is (m.a.w. of dat getal tussen \(-1\) en \(1\) ligt). Dat doen we door de and te gebruiken om 2 eenvoudige booleaanse expressies met elkaar te combineren.

def is_sin(x):
    return (-1 <= x) and (x <= 1) 
is_sin(-5)
False
is_sin(-0.6)
True

We kunnen is_sin overigens ook als volgt schrijven. Python laat toe dat je de twee relationele operatoren combineert in één enkele expressie die veel meer leesbaar is.

def is_sin2(x):
    return -1 <= x <= 1
is_sin(-5)
False
is_sin(-0.6)
True

and en or zijn lazy

Indien we terug kijken naar de definitie van or, dan zien we dat het resultaat steeds True is van zodra het eerste operand True is. Vandaar dat or op een “slimme” manier werkt en zich de moeite niet meer getroost om het tweede operand uit te voeren van zodra het eerste operand True is. Men zegt dat or een luie (Eng: lazy) operator is. Dat wordt hieronder geïllustreerd.

def luie_or(x):
    return positive(x) or (1/0 == "tof")
luie_or(1)
True
luie_or(-1)
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-79-d3c8d5482a50> in <module>
----> 1 luie_or(-1)

<ipython-input-77-29f0788c78a4> in luie_or(x)
      1 def luie_or(x):
----> 2     return positive(x) or (1/0 == "tof")

ZeroDivisionError: division by zero

We definiëren hier een functie luie_or die (als argumenten van de or) eerst positive zal oproepen en daarna de zinloze bewerking 1/0 == "tof" zal uitvoeren. In het experimentje zien we dat deze zinloze bewerking niet wordt uitgevoerd indien het argument daadwerkelijk een positief getal is. Dat komt omdat or lazy is en zijn rechteroperand niet uitvoert ingeval het linkeroperand True is. Indien het eerste operand echter False oplevert, hangt het antwoord af van het tweede operand (dat in dat geval dus ook uitgevoerd dient te worden) wat een foutmelding oplevert. Dat illustreert de oproep van luie_or met -1.

Ook and heeft een luie betekenis. Kan je deze ontcijferen op basis van volgend experiment? Wanneer precies wordt het tweede operand van and wél en niet uitgevoerd?

def luie_and(x):
    return positive(x) and (1/0 == ":-(")
luie_and(-1)
False
luie_and(4)
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-82-1b6f135b5d5a> in <module>
----> 1 luie_and(4)

<ipython-input-80-c935d5588f3f> in luie_and(x)
      1 def luie_and(x):
----> 2     return positive(x) and (1/0 == ":-(")

ZeroDivisionError: division by zero

Keuzes Maken met if


Na deze omweg over booleaanse waarden, booleaanse operatoren, relationele operatoren en predikaten komen we terug bij het eigenlijke onderwerp van dit hoofdstuk: zélf functies schrijven in Python.

Sommige functies worden immers gedefinieerd door gevalsonderscheiding. Dat zijn definities die we typisch m.b.v. accolades noteren. Een zeer bekend voorbeeld is de definitie van de absolute waarde van een getal (Ptyhon kent abs, we programmeren absolute hier zelf om een idee te illustreren).

\[\begin{split}|x| = \left\{ \begin{array}{ll} x & \mbox{if } x \geq 0 \\ -x & \mbox{if } anders \end{array} \right.\end{split}\]

Zo’n “vertakkingen” in functiedefinities worden in Python m.b.v. het if statement bewerkstelligd. Hieronder zien we de definitie van een functie absolute die de absolute waarde van een getal berekent:

 def absolute(x):
    if x < 0:
        return -x
    else:
        return x
absolute(5)
5
absolute(-5)
5

Algemeen gesproken, is if een statement dat uit drie componenten bestaan: een booleaanse expressie die men de test noemt, een block code die men de then-tak noemt en een block code die men de else-tak noemt. Bij aanvang van het if statement zal Python de test uitvoeren en afhankelijk van het resultaat (True of False) de then-tak dan wel de else-tak uitvoeren.

In het algemeen ziet de if test er dus als volgt uit:

Merk op dat de dubbele punten verplicht zijn, dat de sleutelwoorden if en else op hetzelfde niveau in de tekst dienen in te springen en dat ook alle statements in beide blocks op hetzelfde niveau in te dienen springen.

Ons positive voorbeeld van hierboven zouden we ook als volgt met een if test kunnen schrijven: indien het argument groter of gelijk is aan nul geven we True weer; anders geven we False weer. Het is typerend voor beginnende programmeurs om deze definitie makkelijker te vinden dan de definitie die hierboven werd gepresenteerd. Computerwetenschappers zullen hier doorgaans niet mee akkoord gaan aangezien de eerste definitie korter is en het type bool als een volwaardig algebraisch type behandelt. Maar nogmaals, over smaken en kleuren …

def positive(x):
    if x >= 0:
        return True
    else:
        return False
positive(5)
True
positive(-3)
False

else is optioneel**

In sommige gevallen dient er helemaal niks bijzonders te gebeuren indien de test False is. Om zulke gevallen toe te laten is de else-tak optioneel. Het volgende stukje code laat zien hoe we de absolute waarde nogmaals kunnen schrijven door de else-tak weg te laten.

def absolute2(x):
    if x < 0:
        return -x
    return x
absolute2(5)
5

Merk op dat de body van absolute2 nu een block is dat 2 statements bevat: een if statement en een return statement. Indien je absolute2 oproept zal de body aan het werk beginnen en het eerste statement uitvoeren. Dat is het if statement. Indien de test waar is zal de then-tak worden uitgevoerd en zal er dus teruggekeerd worden naar de oproeper met -x. Indien de test onwaar is wordt er overgegaan naar het volgende statement van de body aangezien de if test geen else-tak heeft. Dit is het return x statement wat in dit geval dus uitgevoerd zal worden.

Het is doorgaans niet zo’n goede gewoonte om de else-tak weg te laten. Zoals hierboven wordt geïllustreerd kan je zulke code niet meer zuiver wiskundig lezen maar dien je werkelijk stap voor stap “computertje te spelen” om te begrijpen wat er gebeurt. Dat stap voor stap nabootsen van alle mogelijke uitvoeringen van een functie is doorgaans veel omslachtiger (en dus ontvankelijker voor fouten) dan het abstract wiskundig redeneren over een functie. Maar ook hier geldt weer het verhaal van smaken en kleuren.

Meerdimensionale Testen

Soms is het resultaat van een functie afhankelijk van meerdimensionale input. Het volgende voorbeeldje toont een functie risk_of_heart_disease die op basis van iemands BMI en leeftijd berekent of het risico op een hartaandoening laag, medium of hoog is. De functie verwacht twee argumenten (een int en een float) en produceert een string als resultaat. De werking van de functie kunnen we samenvatten in de volgende tweedimensionale tabel:

Leeftijd/BMI

\(<45\)

\(\geq 45\)

\(\leq 22.0\)

Laag

Medium

\(>22.0\)

Medium

Hoog

Om zulke meerdimensionale testen te schrijven dienen we testen in elkaar te nesten. Onze functie risk_of_heart_disease heeft dus een body die uit één enkel statement staat, namelijk een if statement dat beslist of we in de linker- dan wel de rechterkolom van de tabel zitten. Zowel de then-tak als de else-tak van dat if statement bestaan opnieuw uit een if statement dat de beslissing maakt of we in de onderste dan wel de bovenste rij van de tabel zitten. Alle combinaties tesamen genomen, zijn er dus \(4\) verschillende manier waarmee Python onze functie met een string kan verlaten.

def risk_of_heart_disease(age, bmi):
    if age < 45:
        if bmi <= 22.0:
            return "low"
        else:
            return "medium"
    else:
        if bmi <= 22.0:
            return "medium"
        else:
            return "high"

Computerwetenschappers gebruiken de term “nesten” telkens ze een samengesteld concept zien waarvan de samenstellende delen opnieuw dat zelfde concept bevatten. Hier is dat een if statement dat op zijn beurt if statements bevat. We hebben het eerder bijvoorbeed al over geneste tupels gehad. Dat zijn tupels waarvan de componenten op hun beurt tupels zijn. We zullen in de volgende topics nog verschillende voorbeelden van nesting tegenkomen.

Meerdere Takken: elif

Tenslotte gebeurt het vaak dat het resultaat van een test meer dan twee resultaten kan opleveren. Beschouw volgende functie category die voor een gegeven pH-waarde de correcte benaming van de chemische stof teruggeeft. We spreken over sterke zuren, zwakke zuren, neutrale stoffen, zwakke basen en sterke basen. Deze functie bestaat uit verschillende if statements die in mekaar genest zijn. Ieder if statements vangt “de volgende” mogelijke uitkomst op en vermindert dus het aantal nog mogelijke uitkomsten. Het is belangrijk de neststruktuur van de functie te begrijpen: het is een if statements genest in de else-tak van een if statements dat genest is in de else-tak van een if statements dat genest is in de else-tak van een if statements dat genest is in de else-tak van een if statements!

def category(pH):
    if 0 <= pH <= 4:
        return "strong acid"
    else:
        if 5 <= pH <= 6:
            return "weak acid"
        else:
            if pH == 7:
                return "neutral"
            else:
                if 8 <= pH <= 9:
                    return "weak base"
                else:
                    if 10 <= pH <= 14:
                        return "strong base"
                    else:
                        return "unknown ph"
category(7)
'neutral'

Het hoeft geen verder betoog dat dit zeer onhandig is. Het veelvuldig nesten van if statements geeft moeilijk te lezen code die bovendien bijzonder makkelijk fout te interpreteren is. Merk verder op dat Python eist dat alles correct ingesprongen is. Eén spatietje te veel of te weinig maakt dat Python zich verslikt in de read fase!

Eigenlijk is bovenstaande code conceptueel gezien slechts een lange rij van opeenvolgende testen die (in geval van True) elk hun eigen tak hebben. Er is dus eigenlijk slechts één echte else-tak. Om zulke gevallen op te vangen laat Python een onbeperkt aantal elif takken toe tussen de then-tak en de else-tak. elif is een afkorting voor “else if”. Hieronder zien we een tweede versie van de functie category die gebruik maakt van elif:

def category2(pH):
    if 0 <= pH <= 4:
        return "strong acid"
    elif 5 <= pH <= 6:
        return "weak acid"
    elif pH == 7:
        return "neutral"
    elif 8 <= pH <= 9:
        return "weak base"
    elif 10 <= pH <= 14:
        return "strong base"
    else:
        return "unknown ph"
category(7)
'neutral'

Merk op dat er een dubbele punt nodig is na elke test. Hieronder zien we dus de algemene structuur van het if statement in Python. Zowel de elif takken als de else tak zijn optioneel. Het enige wat dus absoluut verplicht is, is het sleutelwoord if, de eerste test, de dubbele punt en vervolgens een ingesprongen block dat uit minstens één statement bestaat.

Functies vs. Procedures


In sectie 2 hebben we al het onderscheid tussen functies en procedures aangeraakt. Voor Python als programmeertaal zijn functies en procedures echter precies hetzelfde. Het zijn gewoon twee “soorten” van functies:

  • Functies worden opgeroepen vanuit expressies en leveren dus een waarde op. Indien een functie echter wordt opgeroepen en de evaluator komt tijdens de uitvoering van de body nergens een return statement tegen, dan “valt” Python uit de functie en wordt er eigenlijk impliciet None als waarde teruggegeven.

  • Procedures worden opgeroepen als statements (men verwacht immers toch niet dat ze een waarde teruggeven). Indien de aanroep van een procedure echter toch onverwacht aanleiding geeft tot het evalueren van een return statement dan negeert Python de eventueel teruggegeven waarde.

Hieronder zien we enkele stukjes Python die de grenzen aftasten van hoe we functies, if statements en return statements op zinvolle manier kunnen combineren. Het begrip van deze stukjes code is belangrijk om jezelf ervan te vergewissen dat je alle begrippen goed begrepen hebt.

def f(x):
    x
f(5)
type(f(5))
NoneType
None==f(5)
True

In de functie f werd het return statement vergeten. We noemen deze functie dus een procedure aangezien ze geen resultaatwaarde oplevert. Dat zien we hieronder bij de aanroep van f(5). We zien ook dat evaluatie van type(f(5)) netjes NoneType (t.t.z. het type van None) weergeeft. Er komt dus wel degelijk iets uit f, alleen print de REPL het niet uit.

Hieronder volgen 3 pogingen om een functie te schrijven die het maximun weergeeft van haar 2 parameter.

 def max1(x,y):
    if x > y:
        return x
    else:
        return y
max1(2,7)
7
max1(7,2)
7

De max1 functie bevat een if statement met een else-tak. Wanneer de conditie x > y True oplevert wordt x teruggegeven. Wanneer de conditie False oplevert wordt y teruggegeven. De functie werkt correct.

def max2(x,y):
    if x > y:
         return x
    return y
max2(2,7)
7
max2(2,7)
7

De max2 functie bevat een if statement zonder else-tak. Wanneer de conditie x>y True oplevert wordt x teruggegeven. Wanneer de conditie echter False oplevert is de if test afgelopen want er is geen else-tak. Python gaat dan gewoon over naar het volgende statement return y waardoor de max2 ook hier het juiste resultaat geeft.

def max3(x,y):
    if x > y:
        return x
        return y
max3(2,7)

De max3 functie lijkt heel sterk op de max1 functie. Het enige verschil is dat het statement return y anders is ingesprongen! Deze functie heeft een body die slechts uit één statement bestaat: een if statement waarvan de then-tak een block is met 2 opeenvolgende return statements. Uiteraard zal het tweede return statement nooit worden uitgevoerd aangezien de evaluator de functie verlaat van zodra het eerste return statement wordt uitgevoerd. Dat zien we bij de oproep van max2(7,2) waar we tich de x terugkrijgen. Maar het loopt mis bij de oproep van max3(2,7). Vermits de conditie False oplevert gaat Python naar hetvolgende statement. Dat statement bestaat niet en dus vallen we uit max3 met None als terugkeerwaarde.

Gevalstudies


In deze sectie laten we enkele gevalstudies zien die de kracht van Python functies aantonen. Het is de bedoeling dat je de code echt tot op het bot ontcijfert en dat je precies begrijpt welke van de begrippen uit dit hoofdstuk we in ieder voorbeeld gecombineerd hebben.

Wiskunde: Kwadratische Vergelijkingen

We beginnen met het oplossen van vergelijkingen van de vorm \(a.x^2+b.x+c = 0\). Bedoeling is een functie quadratic te schrijven, die gegeven \(a\), \(b\) en \(c\), de wortel(s) oplevert. Iedereen weet dat we daartoe dienen \(\Delta = b^2-4ac\) te berekenen en dat er afhankelijk van het teken van \(\Delta\) geen, één of twee wortels zijn. Hier is de Python code die dat voor ons doet:

from math import *

def quadratic(a,b,c):
    delta = b*b - 4.0*a*c
    if (delta == 0):
        return - b / (2.0 * a)
    elif delta > 0:
        return ((- b + sqrt(delta)) / (2.0 * a),
                (- b - sqrt(delta)) / (2.0 * a))
    else:
        return None

We focussen op de structuur van de code. De functie quadratic heeft een body die uit 2 statements bestaat: een toekenning gevolgd door een if test. De toekenning introduceert een lokale variabele delta. De if test heeft 3 takken. Indien delta == 0 de waarde True oplevert, keert de functie terug met één wortel, namelijk een getal. Indien deze test False oplevert, wordt de elif test geëvalueerd (t.t.z. delta > 0). Indien hier True uit komt, geeft de functie een 2-tupel terug waarin beide wortels als componenten zijn opgenomen. Na een oproep van de vorm roots = quadratic(-1,10,10) krijgen we bijvoorbeeld toegang tot de wortels door roots[0] en roots[1] te evalueren. Ten slotte wordt de waarde None expliciet weer gegeven indien \(\Delta<0\). Merk op dat dat laatste niet strikt noodzakelijk is. Python zou de waarde automatisch weergeven indien we – door afwezigheid van een else-tak – uit de functie zouden vallen. Code die dit expliciteert is echter duidelijker leesbaar.

Fysica: Vectorrekening

In de fysica is het rekenen met vectoren één van de meest voorkomende wiskundige hulpmiddelen. Indien we bijvoorbeeld een truck een berg laten oprijden met een bepaalde snelheid dan kan deze snelheid worden voorgesteld door een 3-dimensionale vector \(\vec{v}_{truck}\) die 3 componenten heeft. Dat zijn 3 getallen die de snelheid in elke richting aanduiden. Indien intussen iemand op de oplegger van die truck wandelt met een bepaalde snelheid, dan wordt ook deze snelheid voorgesteld door een 3-dimensionale vector \(\vec{v}_{iemand}\). Om de totale snelheid van die persoon ten opzichte van de berg te kennen, dienen we beide vectoren componentsgewijs bij elkaar op te tellen: \(\vec{v}_{truck} + \vec{v}_{iemand}\).

In Python kunnen we 3-dimensionale vectoren voorstellen door 3-tupels. We kunnen echter niet zomaar + gebruiken om de componentsgewijze optelling van vectoren te realiseren aangezien dat een 6-tupel oplevert. Hier is een functie vector_sum die de vectoriële som correct berekent. Het resultaat ervan is een 3-tupel waarvan de componenten ieder uit de som van de corresponderende componenten van de 2 gegeven vectoren bestaan.

def vector_sum(v1,v2):
    return (v1[0]+v2[0],v1[1]+v2[1],v1[2]+v2[2])

We kunnen ons arsenaal aan functies voor vectorieel rekenen uitbreiden met vector_scal en vector_inpr. De eerste berekent de vector \(k.\vec{v}\) indien \(\vec{v}\) een vector is en \(k\) een scalair. De tweede berekent het inproduct \(\vec{v}_1 \times \vec{v}_2\) van twee vectoren.

def vector_scal(k,v):
    return (k*v[0],k*v[1],k*v[2])
def vector_inpr(v1,v2):
    return v1[0]*v2[0]+v1[1]*v2[1]+v1[2]*v2[2]

We kunnen deze nu gebruiken om bijvoorbeeld de snelheid te berekenen van een deeltje. Stel dat een deeltje zich beweegt van een plaatsvector \(\vec{p}_1\) naar een plaatsvector \(\vec{p}_2\) tussen tijdstippen \(t_1\) en \(t_2\). De snelheidsvector die hiermee overeenkomt is dus:

\[\vec{v} = \frac{\Delta \vec{r}}{\Delta t}\]

waarbij \(\Delta \vec{r} = \vec{p}_2 - \vec{p}_1\) en waarbij \(\Delta t = t_2 - t_1\).

Vermits we weten dat \(\vec{v}_2 - \vec{v}_1\) eigenlijk hetzelfde is als \(\vec{v}_2 + (-1.\vec{v}_1)\) en dat \(\frac{\Delta \vec{r}}{\Delta t}\) eigenlijk hetzelfde is als \(\frac{1}{\Delta t} . \Delta \vec{r}\) krijgen we de volgende Python functie. p1 en p2 stellen de beide plaatsvectoren voor als een 3-tupel. Het resultaat is een 3-tupel dat de snelheidsvector voorstelt.

def velocity(p1,p2,t1,t2):
    delta_t = t2 - t1
    delta_r = vector_sum(p2,vector_scal(-1.0,p1))
    v = vector_scal(1.0/delta_t,delta_r)
    return v

Chemie: Edelgassen Bepalen

In de volgende gevalsstudie schrijven we een functie edelgas die het atoomnummer van een element als enig argument neemt. Bedoeling is dat de functie ofwel True, ofwel False weergeeft afhankelijk van het feit of het element met dat atoomnummer een edelgas is. Hier is onze eerste poging:

 def edelgas1(Z):
    if Z==2:
        return True
    elif Z==10:
        return True
    elif Z==18:
        return True
    elif Z==54:
        return True
    elif Z==86:
        return True
    elif Z==118:
        return True
    else:
        return False

Deze functie is technisch helemaal correct. Nochtans zal iedere computerwetenschapper het een functie vinden die van weinig goede smaak getuigt. De volgende functie is helemaal equivalent maar bovendien al iets leesbaarder:

def edelgas2(Z):
    return Z==2 or Z==10 or Z==18 or Z==36 or Z==54 or Z==86 or Z==118

Hier gebruiken we de == operator 7 keer. Iedere toepassing van de operator zal True of False opleveren. De resultaten van deze testen worden twee per twee gecombineerd met or. Herinner dat de or lui is. Eens een test True oplevert zullen de andere testen (van links naar rechts) niet meer uitgevoerd worden

De derde versie van onze functie is de meest stijlvolle. Hier gebruiken we de in operator. De in operator verwacht 2 argumenten: een element aan de linkerkant en een tupel aan de rechterkant. De in operator geeft True als en slechts als het element voorkomt in het tupel.

def edelgas3(Z):
    return Z in (2,10,18,36,54,86,118)

Alle drie de “implementaties” van de functie edelgas nemen een getal als argument en produceren een booleaanse waarde als resultaat. De functies laten zien dat er dikwijls verschillende programmeerstijlen mogelijk zijn om hetzelfde te bereiken. Doorgaans verkiezen computerwetenschappers de stijl die de kortst mogelijke code oplevert en die “gewoon leest” zonder dat je de evaluator moet “nabootsen” om te begrijpen wat er gebeurt. Dat is precies waarin de laatste implementatie verschilt van de eerste implementatie.

Functies Debuggen met print


Python functies zijn eigenlijk “zwarte dozen” waar je argumenten in stopt en waar een resultaat uitkomt. Als oproeper van een functie wil je je niet bezig houden met de manier waarop de functie de resultaten berekent. Dat hebben we procedurele abstractie genoemd. Maar wat als het fout gaat? Wat als je de functie oproept en je krijgt resultaten terug die niet overeenstemmen met wat je verwacht? In zo’n geval heb je te maken met een bug oftewel programmeerfout. De mythe wil dat de eerste computers die met radiolampen werden gebouwd verkeerde resultaten gaven indien er een insect op zo’n radiolamp zat. De mythe is haast zeker onwaar maar toch is het woord blijven bestaan in de computerwetenschappen. Het zoeken en verwijderen van programmeerfouten noemt men debuggen.

Beschouw volgende functie bad_quadratic om vierkantsvergelijkingen op te lossen.

def bad_quadratic(a,b,c):
    delta = b*b - 4*a*c
    if (delta == 0):
        return -b // (2*a)
    elif delta > 0:
        return ((-b + sqrt(delta)) / (2*a),
                (-b - sqrt(delta)) / (2*a))
    else:
        return None

Hier is wat er gebeurt indien we de vergelijking \(4x^2 + 4x+1 = 0\) proberen op te lossen.

bad_quadratic(4,4,1)
-1
None==print(4)
4
True

De uitkomst is niet wat we verwachten. We zien immers dat \(\Delta = 14-14 =0\) wat betekent dat de wortel \(-4 / 8 = -0.5\) moet zijn. Waar is het misgelopen?

Om te kunnen weten wat een functie allemaal aan het uitspoken is alvorens het resultaat wordt teruggegeven bestaat er de print functie. Elke oproep van print zet iets op het scherm, zélfs indien de REPL tijdelijk buiten spel staat omdat de body van de functie nog in de evaluatiefase zit.

Hieronder zien we de print functie aan het werk. Eigelijk is het een procedure, ze laat tekst op het scherm verschijnen maar ze geeft None terug.

Hieronder staan er een aantal opeenvolgende voorbeelden. Als je print oproept met één enkel argument wordt die waarde op het scherm uitgeschreven. De volgende oproep van print begint dan aan een nieuwe lijn. Als print wordt opgeroepen met meerdere argumenten worden die naast elkaar op 1 lijn uitgeschreven met bij default één spatie er tussenin. Wil je een andere separator kan je dat explciet aangeven in een optioneel argument sep. Als je wil dat er na een print niet naar de volgende lijn wordt gesprongen kan je dat ook aangeven met de optionele parameter end. De lijn wordt dan niet beëindigd met een “ga naar de nieuwe lijn” maar met de meegegeven waarde.

x=2
print(1)
print(x)
print(1+x)
print(4,5)
print((4,5))
print("resultaat=", 6)
print(7,"< resultaat <",7)
print("jan","willem")
print("jan","willem", sep="-")
print("jan", end=" ")
print("willem")
1
2
3
4 5
(4, 5)
resultaat= 6
7 < resultaat < 7
jan willem
jan-willem
jan willem

We schrijven als voorbeeld een procedure verbose die een string als argument neemt en dan een korte conversatie op het scherm laat zien alvorens terug te keren naar de REPL met de string "Voila!". Zorg dat je goed begrijpt wat er gebeurt! Merkt op dat de 5 lijnen tekst op het scherm het resultaat zijn van de oproepen van print. De string "Voila!" is de return waarde van de functie verbose.

def verbose(name):
    print ("Hello", " ", name, ",", sep="")
    print ("I'm Python.")
    name1 = "Wolf"
    name2 = "Viviane"
    opinion = "are really cool folks."
    print ("Here's what I was just thinking:")
    print (3*" ", name1, "and", name2, end=" ")
    print (opinion)
    print ("Going back to the REPL now. Bye!")
    return "Voila!"
verbose("Mr. President")
Hello Mr. President,
I'm Python.
Here's what I was just thinking:
    Wolf and Viviane are really cool folks.
Going back to the REPL now. Bye!
'Voila!'

Hoe kunnen we nu print gebruiken om te zien wat er verkeerd was aan onze bad_quadratic functie? Ligt de fout bij het berekenen van delta of bij het berekenen van de expressie in de return statement dat werkt voor het geval dat er 1 wortel is? We zetten daarom een print statement meteen na de berekening van delta om de waarde van delta te controleren en nog 1 net vóór het return statement waarmee we de noemer van de breuk controleren.

def bad_quadratic(a,b,c):
    delta = b*b - 4*a*c
    print ("delta =", delta)
    if (delta == 0):
        print("2*a =", 2*a)
        return -b // (2*a)
    elif delta > 0:
        return ((-b + sqrt(delta)) / (2*a),
                (-b - sqrt(delta)) / (2*a))
    else:
        return None
bad_quadratic(4,4,1)
delta = 0
2*a = 8
-1

We zien nu datdelta correct wordt berekend en de noemer van de breuk is eveneens correct. Dat betekent dus dat het misloopt bij de deling. We hebben de bug gevonden! We hebben ons bij hegt programmeren vergist en // getikt wat in Python de gehele deling is. -4 // 8 geeft als uitkomst -1 en niet de verwachtte 0.5 Het euvel is makkelijk verholpen door de gewone / operator te gebruiken. Dan zal de uitkomst van de deling een float getal worden. Probeer het!

Samenvatting


In dit topic hebben we geleerd van zelf functies te schrijven. We hebben besproken hoe we op basis van argumenten een resultaat kunnen teruggeven m.b.v. het return statement. Dat staat in de body van de functie die typisch uit meerdere statements kan bestaan. Deze vormen dan een block. Sommige van deze statements kunnen toekenningen zijn en dat geeft dan weer aanleiding tot lokale variabelen. Om onze body wat interessanter te maken hebben we het if statement bestudeerd. Dat bestaat uit een conditie en een then-tak, een aantal optionele elif takken en een optionele else-tak. De conditie is een booleaanse expressie. Dat is een expressie die een booleaanse waarde oplevert en die willekeurig complex kan zijn door meerdere booleaanse expressies te combineren m.b.v. booleaanse operatoren. Tevens hebben we relationele operatoren gezien. Dit zijn eigenlijk ingebouwde predikaten die een relatie prediken over twee argumenten. We hebben alles uitvoerig geïllustreerd in drie relevante gevalstudies. Ten slotte hebben we het print statement gezien dat ons toelaat dingen op het scherm te zetten tijdens de uitvoering van een functie. Je kan dit statement gebruiken om Python functies te debuggen.