Dette eksemplet bygger videre på rpncalc1 og viser hvordan en kan bruke funksjoner for å gjøre et program ryddigere.

Definisjon og bruk av funksjoner

Funksjoner kan ses på som en innkapsling av programkode, med flere viktige fordeler:

  • Ved å sette navn på et stykke kode og bruke navnet i stedet, så gjøres resten av koden ryddigere og lettere å lese.
  • Parametre og retur-verdier tydeliggjør hvilke verdier et stykke kode er avhengig av og har som resultat.
  • En funksjon kan kalles fra flere steder, altså en slags gjenbruk, og gjør programmet mer kompakt.

Den vanskeligste delen er å finne ut hvilke funksjoner som er nyttigst, altså hvilke funksjoner gjøre koden ryddigst og gir mest gjenbruk. En god test er om det er lett å finne et fornuftig navn og skille koden fra resten ved å angi parametre og returverdi.

Ta koden fra rpncalc1.py for sjekke om en tekst er en operand (og ikke en operator) som eksempel: token[0].isdigit(). Det er ikke opplagt hva hensikten er, men ved f.eks. å kalle koden isOperand, så gjøres dette tydeligere. Koden blir som følger:

def isOperand(token):
    return token[0].isdigit()
...
while (True):
    ...
    token = raw_input(" > ")
    if isOperand(token):
        operand = float(token)
	...

Øverst defineres isOperand-funksjonen: Den tar inn en tekst (string) og returnerer en sannhetsverdi (boolean, altså True eller False).

Merk at sannhetsverdien som isOperand returnerer kun er avhengig av argumentet som overføres, fordi funksjonskoden (som ofte kalles funksjonskroppen) ikke refererer til noen variabler utenfor funksjonen, f.eks. operands. Funksjonen har heller ingen side-effekter, dvs. den endrer ingenting i omgivelsene sine. Dette betyr at funksjonen er en matematisk funksjon, fordi den alltid returnerer samme verdi for samme sett av argumenter. Slike funksjoner er enklere å analysere bruken av, fordi de ikke er avhengig av omgivelsene.

Lenger ned brukes av isOperand: token overføres som argument returverdien brukes direkte som betingelsen i if-setningen.

Et problem med originalkoden i rpncalc1.py var at den ikke sjekket om det var nok operander tilgjengelig før den pop-et verdier i elif-grenen for pluss- og minus-operatorene. Disse bør begge sjekke om lista som operands-variablen refererer til har mange nok verdier. Siden begge grener har behov for å sjekke dette, så kan en oppnå gjenbruk ved å definere en funksjon som gjør sjekken:

operands = []
...
def hasOperands(n):
    return len(operands) >= n
...
while (True):
	...
    elif token == "+":
		if hasOperands(2):
			...
    elif token == "-":
		if hasOperands(2):
			...
	...

hasOperands returnerer True dersom operands-lista er minst så mange verdier som angitt i argumentet og False om så ikke er tilfelle.

Denne funksjonen er ikke en matematisk funksjon, fordi resultatet er avhengig av verdier utenom argumentene, nemlig operands-variablen.

hasOperands-funksjonen brukes i elif-grenene for både pluss- og minus-operatorene.

Til sist definerer vi funksjoner for hver av de to operatorene pluss og minus, for å gjøre løkke-koden enda ryddigere:

operands = []
...
def plus():
    if hasOperands(2):
        operands.append(operands.pop() + operands.pop())

def minus():
    if hasOperands(2):
        operands.append(- (operands.pop() - operands.pop()))
...
while (True):
	...
    elif token == "+":
        plus()
    elif token == "-":
        minus()
	...

Her defineres en funksjon for hver av operatorene, for å forenkle løkke-koden. Vi får ingen gjenbruk av kode, fordi hver av funksjonene bare kalles ett sted. Begge disse funksjonene er avhengig av omgivelsene sine, både fordi de bruker en annen funksjon som er det, og fordi de selv refererer til en variabel utenfor seg selv, nemlig operands. I tillegg har begge side-effekten at lista som operands refererer til endres, dersom den er har minst to operander.

Ser du forresten hvordan feilen med minus-logikken i rpncalc1.py er rettet?

 

Hva skjer nå funksjoner kalles?

I rpncalc1-eksemplet viste vi hvordan variabler noteres på et slags ark som opprettes når Python-programmet kjøres. Noe lignende skjer når en funksjon kalles: Det opprettes et lite ark, tenk Post-It-lapp, hvor parametre og lokale variabler noteres. Dette lille arket kommer på toppen av program-arket, slik at koden inni funksjonen kan referere til variabler på både det lille funksjons-arket og det store program-arket. Figuren under illustrerer situasjonen når hasOperands er kalt med 2 som argument:

rpncalc2operands = []token = "+"operand = ...1: hasOperandn = 2

Når hasOperand er kalt, så har vi både et rpncalc2-ark for programmet som helhet og et hasOperand-ark for funksjonen. Koden inni funksjon vil kunne referere til variabler i begge arkene. Så når verdien av uttrykket len(operands) >= n beregnes, så finner den verdien av operands i rpncalc2-arket og n i hasOperand-arket.

Pila i figuren angir at hasOperand-arket sjekkes først, og deretter rpncalc2-arket. Dersom vi hadde kalt parameteret til hasOperand for operands (uvisst av hvilken grunn ...), så ville vi ikke kunnet referere til operands på rpncalc2-arket, fordi den ville funnet operands i hasOperands-arket først.

Når hasOperand returnerer så fjernes hasOperand-arket og parametrene og de lokale variablene blir borte. Det er derfor det er nyttig å tenke på funksjonsark som Post-It-lapper, fordi de legges på og fjernes hele tiden.

Reglene for hvordan funksjonsark kommer på toppen av eksisterende ark er litt mer komplisert enn dette. Hvorfor er det f.eks. slik at hasOperand-arket refererer til rpncalc2-arket? Regelen er at funksjonsarket refererer til samme ark som funksjonen selv er definert i! rpncalc2-arket inneholder strengt tatt ikke bare variabler, men også funksjoner (evt. kan en se på funksjoner som variabler som har funksjoner som verdi), slik vi viser i figurene under. Disse viser hva som skjer når plus-funksjonen kalles og videre kaller hasOperands:

rpncalc2operands = []hasOperands(n)plus()token = "+"operand = ...1: plusplus-funksjonen er akkurat kalt fra elif-grenen i while-løkka. plus-arket refererer til rpncalc2-arket, fordi plus-funksjonen selv er definert i rpncalc2-arket. Dermed kan koden i plus-funksjonen referere til variabler (og funksjoner) i rpncalc2-arket.
rpncalc2operands = []hasOperands(n)plus()token = "+"operand = ...1: plus1: hasOperandsn = 2plus-funksjonen har akkurat kalt hasOperands. Vi har nå to aktive funksjonsark, den stiplete linjen angir at plus-arket har aktivert hasOperands-arket. Merk hvordan begge refererer til rpncalc2-arket, fordi både plus- og hasOperands-funksjonene selv er definert der. Men selv om hasOperands-arket ble aktivert av plus-funksjonen, så refererer det ikke til plus-arket.

Hovedprogram-funksjon

Selv om programmet nå virker som det skal og er forholdsvis ryddig, så skal vi gjøre en viktig endring til. Som figurene over viser, så er token- og operand-variablene som brukes inni while-løkka, like globale og synlige for andre funksjoner som operands-lista, selv om det kun er operands-lista som det er meningen at andre funksjoner skal kunne lese og endre. En måte å gjøre løse det på er å legge while-løkka i en egen funksjon, slik at token og operand blir lokale for denne funksjonen. Siden dette er hovedfunksjonen til programmet kalles den gjerne main. Den eneste koden som da til syvende og sist ligger utenfor en funksjon er initialiseringen av operands og et enkelt kall til main(), som vist til venstre under. Til høyre for koden er det illustrert hvordan "arkene" vil se ut når plus-funksjonen har kalt hasOperands, tilsvarende figuren over. Vi ser at token- og operand-variablene nå ligger inni main-arket, som er utilgjengelig for plus- og hasOperands-arkene.

operands = []

def isOperand(token): ...
def hasOperands(n): ...

def plus(): ...
def minus(): ...

def main():
	while (True):
    	...
	print("program exited")

main()
rpncalc2operands = []hasOperands(n)plus()main()1: maintoken = "+"operand = ...1: plus1: hasOperandsn = 2

Hele rpncalc2.py

For ordens skyld, nedenfor er hele rpncalc2-koden. Kjør den gjerne og sjekk at den virker! Har vi plantet noen feil i koden denne gangen?

# rpncalc2.py
operands = []

def isOperand(token):
    return token[0].isdigit()

def hasOperands(n):
    return len(operands) >= n

def plus():
    if hasOperands(2):
        operands.append(operands.pop() + operands.pop())
def minus():
    if hasOperands(2):
        operands.append(- (operands.pop() - operands.pop()))

def main():
	while (True):
    	print(operands)
    	token = raw_input(" > ")
    	if isOperand(token):
    	    operand = float(token)
    	    operands.append(operand)
    	elif token == "exit":
    	    break
    	elif token == "+":
    	    plus()
    	elif token == "-":
    	    minus()
    	else:
    	    print("Unsupported operator: " + token)
	print("program exited")

main()