H6. Eigen functies

Inleiding

In de vorige cursus beschreef ik hoe je basisfuncties kunt gebruiken (voorbeelden zijn print(), pow(), input() en int()) en hoe je functies kunt importeren uit modules (voorbeelden zijn de functies math.sqrt(), np.array() en plt.plot(), waarbij modulenamen vaak afgekort worden geschreven). Dit hoofdstuk beschrijft hoe je je eigen functies kun maken. Zelf modules creëren kan ook, maar valt buiten het doel van deze cursus. Om programma’s te schrijven hebben we eigen functies eigenlijk niet per se nodig, net zoals we in theorie zelfs geen basisfuncties of functies uit modules hoeven te gebruiken. Met combinaties van proces-, selectie- en iteratieblokken kunnen we het voorgeschotelde probleem altijd oplossen. Hopelijk zie je ondertussen echter wel in dat gebruikmaken van reeds geschreven code heel gebruiksvriendelijk is. Door eigen functies te schrijven gaan we hiermee nog een stap verder.

Het nut van functies

Waarom zou je functies willen creëren? Er zijn een hoop goede redenen, maar ik focus hier op ééntje: herbruikbaarheid. Stel, je hebt een bepaalde functionaliteit nodig op meerdere plekken in je programma. Als voorbeeld neem ik even de berekening van de vierkantswortel van de som van twee kwadraten: \(a = \sqrt{b^2 + c^2}\)

Binnen driehoeksmeetkunde hebben we deze formule, de stelling van Pythagoras, heel regelmatig nodig. Het kan dus goed zijn dat we onderstaand stuk code verschillende keren moeten typen. Met kopiëren en plakken komen we er wel. Akkoord, voor enkele keren misschien, maar willen we echt 100 keer dezelfde code kopiëren en plakken? Dit moet efficiënter kunnen. Inderdaad, met behulp van eigen functies.

import math

# aanmaak variabelen
b = 3
c = 4

# berekeningen
if b>=0 and c>=0:
	a = math.sqrt( b**2 + c**2 )
else :
	a = math.nan
	print ( 'Let op , minstens een zijde heeft een negatieve lengte !' )

Het maken van eigen functies

Eerder zagen we al dat iedere functie een naam heeft, nul of meer argumenten, en gewoonlijk een retourwaarde. Als je je eigen functies maakt, moet je al deze elementen definiëren. Voor een eigen functie met twee argumenten, gebruik je de volgende syntax:

def functienaam( argument1, argument2 ):
	acties
	return retourwaarde

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 argumentenlijst bestaat uit nul of meer variabele namen, met komma’s ertussen. Hierboven heeft de functie functienaam() twee argumenten. De functiedefinitie start altijd met het keyword def, en eindigt met een dubbelpunt, zoals bij condities (if statements) en loops. Het blok code onder de functiedefinitie moet inspringen. Gewoonlijk eindigen de acties met een return statement, gevolgd door een variabelenaam die de retourwaarde geeft.

Let erop dat Ipython je functie definitie moet hebben ‘gelezen’ voordat je de functie kunt aanroepen. Tijdens dat lezen voert Ipython nog niets uit, maar onthoudt de functienaam en bijhorende acties om later te kunnen gebruiken. Het is daarom een goede gewoonte dat functies bovenin een programma staan, meteen onder de import statements en het definiëren van variabelen.

Net zoals we variabelen als dozen voorstelden in een garage (het werkgeheugen van de computer), gemarkeerd met variabelenamen en gevuld met een variabelewaarde, kun je een gelijkaardige analogie toepassen op functies. Alleen, functies zijn geen dozen om op te slaan maar machines om uit te voeren. Bij het lezen van de eigen functie wordt een machine aangemaakt die een opeenvolging van acties kan uitvoeren, en een waarde kan retourneren. Op de voorkant van de machine schrijven we niet enkel de functienaam, maar ook de nodige argumenten om de machine later aan de praat te krijgen.

Die machine-analogie helpt te onthouden dat een functie, eens gedefiniëerd, gewoon wordt gestockeerd voor later gebruik. Het staat tussen de dozen en andere machines op een rek in onze garage. Bij het definiëren wordt er niets uitgevoerd, enkel gestockeerd. Eens de machine er staat, is het gebruik van eigen functies gelijk aan die van basisfuncties. We roepen via de naam en argumenten de eigen functie aan en laten die zo ratelen en pruttelen om een retourwaarde te genereren.

Een eenvoudig voorbeeld:

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. Dat Python toch eerst de tekst “Hallo!” afdrukt, is omdat 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 gestockeerd – Python onthoudt dat die functie bestaat, zodat hij kan worden uitgevoerd als het hoofdprogramma (of een andere functie) de functie aanroept. Het aanroepen gebeurt pas in de laatste lijn.

Gebruik van argumenten

Bekijk de code hieronder. De code definiëert een functie hallo() met één argument, voornaam geheten. De functie gebruikt voornaam in het print-commando zonder expliciet een waarde te krijgen door middel van de variabele-assignment (=). Dit kan niet, denk je misschien, want er is nooit expliciet een waarde aan voornaam gegeven. Wel, dit is een van de enige uitzondering op de verplichte regel om variabelen éérst een waarde toe te kennen alvorens te gebruiken. voornaam bestaat wel degelijk als variabelenaam omdat het een argument is van de functie; het staat mee gemarkeerd op de doos met de functienaam hallo(). Het heeft geen eigen doos in de garage.

def hallo( voornaam ) :
	print( 'Goeiedag ,', voornaam )

hallo( "Jan" )
hallo( "Wim" )

Tijdens het markeren van de doos hallo( voornaam ) krijgt de variabele voornaam nog geen waarde. Pas wanneer je later de functie aanroept, moet je een waarde geven aan ieder argument van die functie. Dus, om de functie hallo() te aanroepen, moet een waarde voor het argument voornaam gespecificeerd worden als argument tussen de haakjes van de functie. Hier: hallo( "Jan" ).

Het grote voordeel van zo’n “uitgestelde waarde-assignment” is dat de variabele voornaam een nieuwe waarde krijgt telkens wanneer we de functie aanroepen. Een volgende aanroep van de functie kan daarom perfect hallo( "Wim" ) zijn, met de retour ‘Goeiedag, Wim’. Dit zorgt voor uitbreidbaarheid, want we kunnen de functie gebruiken zonder “Goeiedag” te kopiëren en te plakken.

Heel belangrijk is het op te merken dat het hoofdprogramma helemaal geen weet heeft van de argumentnaam in de functie. In het voorbeeld komt voornaam enkel als argument voor en wordt het enkel gebruikt binnen de functie, niét in het hoofdprogramma. De argumenten zijn dus “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. Om vreemde fouten te vermijden, is het daarom zeer belangrijk om geen variabelen te definiëren met dezelfde argumentnaam gebruikt in een van jouw functies.

Return statement

Functie-argumenten worden gebruikt om informatie van buiten de functie naar binnen de functie te communiceren. Vaak wil je ook informatie vanuit de functie naar het hoofdprogramma buiten de functie communiceren. Daartoe dient het return statement. 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. Hoewel dit eigenlijk geen vereiste is, zullen wij ons beperken tot één retourwaarde. Deze waarde wordt dan gecommuniceerd naar het programma buiten de functie. Als je de waardes geretourneerd door een functie wilt gebruiken, moet je zorgen dat ze in het hoofdprogramma in een variabele terecht komen:

# functie blok
def functienaam( argument1, argument2 ) :
	acties
	return retourwaarde

# hoofdprogramma
variabelenaam = functienaam( arg1, arg2 )

Bovenstaande functie heeft de naam functienaam() en aanvaardt twee argumenten. Na de acties zal de functie - eens aangeroepen - een retourwaarde teruggeven. Zolang de functie niet wordt aangeroepen, wordt ze niet uitgevoerd, maar blijft stand-by. In het hoofdprogramma wordt de functie aangeroepen, en krijgt daar de argumenten arg1 en arg2 mee. Met deze ingrediënten schiet onze functiemachine in gang, produceert een retourwaarde en slaat die in het hoofdprogramma op onder de naam variabelenaam.

Concreet voorbeeld

We bekijken het Pythagoras voorbeeld, gebruikmakend van een eigen functie:

import math

# aanmaak variabelen en functies
b = float( input( 'Geef de lengte van de eerste rechthoekszijde: ' ) )
c = float( input( 'Geef de lengte van de tweede rechthoekszijde: ' ) )

def pythagoras( x , y ) :
	if x >=0 and y >=0:
		z = math . sqrt ( x **2 + y **2 )
	else :
		z = math . nan
		print (  Let op , minstens een zijde heeft een negatieve lengte !  )
	return z

# berekeningen en uitvoer
print ( 'Welkom bij de goniometrische rekenmachine!' )
a = pythagoras( b , c ) # de schuine zijde van de rechthoekige driehoek
print ( 'Als b=', b, 'en c=', c, ', dan is a=', a )

Volgende stappen worden achtereenvolgens uitgevoerd:

Twee dingen wil ik via dit voorbeeld nogmaals benadrukken. Ten eerste: de variabelenamen b en c die met de functie-aanroep als argumenten worden meegegeven in het hoofdprogramma zijn niét dezelfde als de namen x en y die in de functie worden gebruikt. Ten tweede: de variabelenaam a waar de retourwaarde in terechtkomt in het hoofdprogramma is niét dezelfde als de naam van de retourwaarde z in de functie.