Lesson 6 — Functions
By the end of this you can: give a piece of work a name, hand it the values it needs, get an answer back out of it, and change a variable that lives outside it — which is the one a game cannot do without.
The same four lines, three times
Here is a program that works and is still wrong:
health = 100
health = health - 30
if health < 0:
health = 0
print("Health:", health)
health = health - 45
if health < 0:
health = 0
print("Health:", health)
health = health - 40
if health < 0:
health = 0
print("Health:", health)
Health: 70
Health: 25
Health: 0
The same four lines, written out three times. Everything you have learnt so far makes programs longer; this is the lesson where they start getting shorter.
And length is the least of it. Suppose you decide that health should stop at 1 rather than 0. There are three places to change and nothing to remind you of the third — so the bug you ship is a player who can survive two hits and dies on the wrong one.
Writing one
A function is a piece of work with a name on it:
def say_hello():
print("Hello!")
print("Ready to play?")
say_hello()
say_hello()
Hello!
Ready to play?
Hello!
Ready to play?
Two separate things happen there, and mixing them up is the commonest confusion in the lesson:
def say_hello():and the indented lines under it define the function. Nothing runs. Python reads it, notes thatsay_hellonow means those two lines, and moves on.say_hello()calls it. Now the lines run.
The shape is the shape you already know from if and for: a line ending in
a colon, and an indented block underneath that belongs to it.
The brackets are what make it happen. Written without them, the name is just the function itself sitting there doing nothing:
def say_hello():
print("Hello!")
say_hello
print("Finished")
Finished
No error, no greeting. A line that is only a name is legal Python and has no effect, which is why a missing pair of brackets is so hard to spot: nothing tells you, the program simply does less than you thought. Keep that one in mind — lesson 7 is built on it.
Giving it something to work with
say_hello says the same thing every time, which is not much use. Put a name
in the brackets and it becomes a blank the caller fills in:
def greet(name):
print("Hello, " + name + "!")
greet("Ada")
greet("Grace")
Hello, Ada!
Hello, Grace!
name is a parameter. Inside the function it behaves like any other
variable; the difference is where its value comes from. greet("Ada") puts
"Ada" into it and then runs the body.
More than one goes in a comma-separated list, and they are filled in order:
def report(name, score, lives):
print(name, "has", score, "points and", lives, "lives")
report("Ada", 120, 3)
report("Grace", 80, 1)
Ada has 120 points and 3 lives
Grace has 80 points and 1 lives
Order, not meaning. report(120, "Ada", 3) runs perfectly happily and prints
nonsense, because Python has no idea which of them was supposed to be a name.
Give the wrong number and it does stop you:
report("Ada", 120)
TypeError: report() missing 1 required positional argument: 'lives'
Which is a friendlier error than most: it names the function, says how many are missing, and tells you which one.
Getting an answer back
So far these functions do something. Often you want one that works something out and hands it back:
def damage(health, hit):
left = health - hit
if left < 0:
left = 0
return left
print(damage(100, 30))
print(damage(20, 45))
70
0
return does two things at once: it sends a value back to whoever called, and
it stops the function immediately. Nothing after a return that has run
will run.
Because it hands a value back, a call can be used anywhere a value can be — stored, printed, added to, passed straight into another call:
def damage(health, hit):
left = health - hit
if left < 0:
left = 0
return left
health = 100
health = damage(health, 30)
health = damage(health, 45)
health = damage(health, 40)
print("Health:", health)
Health: 0
That is the program from the top of the page, with the rule written once. Move the floor from 0 to 1 and there is exactly one line to change.
print and return are not the same thing, and this is worth being
stubborn about. print puts something on the screen for a person to read.
return hands a value back to the program. A function that prints its answer
instead of returning it gives you nothing to work with:
def add_up(a, b):
print(a + b)
answer = add_up(2, 3)
print("I got:", answer)
5
I got: None
The 5 appears — the function printed it — but answer is None. A function
with no return hands back None, which is Python's word for "nothing
here". Seeing None turn up where you expected a number nearly always means a
missing return.
Where names live
A variable made inside a function belongs to that function, and disappears when it finishes:
def roll():
result = 6
print("Inside:", result)
roll()
print("Outside:", result)
Inside: 6
NameError: name 'result' is not defined
This is a feature and not an obstacle. It means you can use i or count or
result inside a function without checking whether some other part of the
program already used that name. Each function gets its own set, and they
cannot collide.
Reading a variable from outside does work:
score = 7
def show():
print("Score is", score)
show()
Score is 7
global — changing something outside
Reading works. Now try changing:
score = 0
def add_point():
score = 1
add_point()
print(score)
0
Read that again, because there is no error and the answer is wrong. The
function ran. It assigned 1. And score is still 0.
Here is why. Assigning to a name inside a function makes that name local to
the function — every time, whether or not a variable of the same name exists
outside. So score = 1 did not touch the outer score at all; it made a
second, private score, put 1 in it, and threw it away when the function
ended.
One word fixes it:
score = 0
def add_point():
global score
score = score + 1
add_point()
add_point()
add_point()
print(score)
3
global score says: when I write score in here, I mean the one outside,
not a new one. It goes at the top of the function, once, and names the
variables — global score, lives for two.
This is the line that makes games work. Everything interesting in a game happens inside a function that the engine calls for you: something is hit, a key goes down, a second passes. Those functions need to change the score, the health, the level. Without
globalthey change a private copy and the game silently never moves — which looks exactly like the collision not being detected, and sends people hunting in completely the wrong place.
You only need it when you assign. Reading needs nothing, and neither does changing something inside an object or a list:
scores = [10, 20]
def add_score(n):
scores.append(n)
add_score(30)
print(scores)
[10, 20, 30]
No global, and it worked. scores.append(30) is not an assignment to
scores — it is a message to the list that scores already points at. Write
scores = [10, 20, 30] in there instead and you are back to the silent local.
Now put it on the screen
A scoreboard with one function that knows how to change it:
from kaypy import *
kaypy(width=800, height=600, background=[20, 24, 36])
score = 0
label = add([text("Score: 0", size=32), pos(40, 40)])
def add_points(amount):
global score
score = score + amount
label.text = "Score: " + str(score)
add_points(10)
add_points(25)
add_points(100)
print("Final score:", score)
Final score: 135
Three calls, one rule. The function is the only place that knows a score is
kept in score and shown in label, so those two can never drift apart —
which they absolutely will if every part of the game updates both by hand.
And it works the same when the caller is the engine rather than you:
from kaypy import *
kaypy(width=800, height=600, background=[20, 24, 36])
score = 0
label = add([text("Score: 0", size=32), pos(40, 40)])
def add_points(amount):
global score
score = score + amount
label.text = "Score: " + str(score)
def tick():
add_points(1)
loop(0.5, tick)
loop(0.5, tick) hands the engine your function and asks it to call it twice
a second. Note the name with no brackets after it: you are giving the
engine the function, not the result of running it. loop(0.5, tick()) would
run tick once, immediately, and hand the engine the None it returned.
Passing a function by name is worth getting comfortable with now, because lesson 7 is an easier way of writing exactly this.
Watch for
Defining is not calling. def only describes the work; the brackets do
it.
A name with no brackets does nothing and says nothing. Look for it when a function seems not to have run.
Arguments go in by position, not by what they are called.
print shows, return hands back. An unexpected None is a missing
return.
Assigning inside a function makes a local unless you said global. This
one fails silently.
Two blank lines before a def, by convention. Not a rule, but every
Python program you meet will be written that way.
Classwork 1 — The Dice Roller
Start here and press Run:
def roll(sides):
return sides
print(roll(6))
print(roll(20))
6
20
It does not roll anything yet — it hands back the number you gave it. Fix that, one step at a time:
- Make it a real roll.
from kaypy import *at the top gives yourandi(1, sides), a whole number between 1 andsides. Return that instead. Run it a few times; the numbers should change. - Write a second function,
roll_twice(sides), that callsrolltwice and returns the total. It must callroll— do not write the randomness again. - Write
best_of_three(sides), which rolls three times and returns the highest.iffrom lesson 3, or a list andmax()from lesson 5. - Count how often a 20-sided die beats 15. Make
wins = 0outside, and a function that rolls once and adds towinswhen it is over 15. Call it fifty times in a loop and printwins.
Step 4 needs global. Try it once without, see the 0, and then add the line —
it is worth meeting that failure on purpose, while you know what it is, rather
than for the first time inside a game.
Stretch: give roll a second parameter, lucky, that adds 1 to the
result when it is True. Then call it both ways from the same program.
Classwork 2 — The Spawner
One function that builds a thing, called over and over.
from kaypy import *
kaypy(width=800, height=600, background=[20, 24, 36])
loadSprite("coin", "images/coin.png")
def make_coin(x, y):
add([sprite("coin"), pos(x, y), area(), "coin"])
make_coin(120, 200)
make_coin(300, 200)
make_coin(480, 200)
- Run it. Three coins. Now add a fourth with one more line, and notice how little that cost.
- Replace the three calls with a
forloop that makes eight coins in a row, 200 apart, using the loop variable to work out thex. - Give
make_coina third parameter,value, and return the objectaddgave you. Store it and print how many you made. - Add a
total_valuevariable outside and havemake_coinadd to it. You will needglobal. Print the total at the end.
Stretch: write make_row(y, how_many) that calls make_coin in a loop,
then call make_row three times at three different heights. A function
calling a function is not a special case — it is the whole point.
Check yourself
What does this print?
lives = 3
def lose_one():
lives = lives - 1
return lives
print(lives)
3
Why is there no error, and why is it 3?
Because lose_one is never called. The def was read, the function was
filed away under that name, and the only line that actually runs is the
print. lives has been 3 the whole time.
The interesting part is what would happen if you added lose_one() above
the print. Not 2, and not a silent 3 either — an error, because
lives = lives - 1 assigns to lives, which makes it local for the whole
function, which means the lives being read on the right-hand side is the
local one, which has nothing in it yet. Python calls that
UnboundLocalError, and the fix is global lives at the top.
So this function has a bug that no amount of running the program will show you, because nothing calls it. Code that is never reached is never tested, which is worth remembering when you write a handler and nothing happens.
← Lesson 5 — Lists · All nine lessons · Lesson 7 — Decorators →