Subroutines, parameters and passing by reference
Out-of-line subroutines and their interfaces, returning several values, and passing by value and by reference.
Do this lesson in the simulatorAt GCSE you wrote functions and procedures with parameters and return values (F4.1 and F4.2). A level asks you to explain precisely what a subroutine is and why it helps, to describe its interface, to return more than one value, and to know what happens to an argument when it is passed by value or by reference.
What a subroutine is
A subroutine is a named, out-of-line block of code that is run by writing its name. Out of line means it is written once, apart from the code that uses it; each call jumps to it, runs it, and comes back to the line after the call. A function returns a value; a procedure does not. Python writes both with def.
The advantages are the ones examiners want by name:
- Reuse: written once, called as often as needed, so a fix is made in one place.
- Decomposition: a large problem becomes a set of small subroutines, each easy to understand.
- Testing: each subroutine can be tested on its own before it is used.
- Teamwork: different people can write different subroutines, agreeing only on how they are called.
- Libraries: tested subroutines can be shared between programs, like the robot's
forwardanddistance.
The interface
The interface of a subroutine is what a caller needs to know to use it: its name, its parameters with their types and meaning, and what it returns. The body can change without breaking any caller, as long as the interface stays the same.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def gap_after(move_cm: float, margin_cm: float = 10) -> float:
"""How far the wall will be after driving move_cm, less a safety margin."""
return distance() - move_cm - margin_cm
print(gap_after(20))
print(gap_after(20, margin_cm=5))
The type hints (float) and the docstring document the interface. margin_cm has a default value, so a caller can leave it out. A parameter is the name in the definition; an argument is the value given in the call.
Returning more than one value
A Python function returns one value, but that value can be a tuple, which the caller unpacks into several variables:
def closest(readings):
best = 0
for i in range(1, len(readings)):
if readings[i] < readings[best]:
best = i
return best, readings[best]
index, cm = closest([31.0, 41.0, 51.0, 19.0, 15.0, 20.0])
print("index", index, "distance", cm)
In AQA's pseudo-code a subroutine is written with SUBROUTINE and returns a value with RETURN:
SUBROUTINE closest(readings)
best ← 0
FOR i ← 1 TO LEN(readings) - 1
IF readings[i] < readings[best] THEN
best ← i
ENDIF
ENDFOR
RETURN best
ENDSUBROUTINE
By value and by reference
When an argument is passed by value, the subroutine gets a copy of the value. Changing the parameter inside the subroutine does not change the caller's variable. When an argument is passed by reference, the subroutine gets the address of the caller's variable. Changing the parameter changes the caller's variable, because both names refer to the same place in memory.
OCR's exam reference language marks each parameter:
procedure nudge(cm:byVal, total:byRef)
cm = cm + 5
total = total + cm
endprocedure
step = 10
driven = 0
nudge(step, driven)
print(step) // 10: cm was a copy
print(driven) // 15: total was the caller's variable
| By value | By reference | |
|---|---|---|
| The subroutine receives | A copy of the value | The location of the caller's variable |
| Changes inside affect the caller | No | Yes |
| Memory and time | The copy costs space and time for large data | No copy, so large data is passed cheaply |
| Safety | The caller's data cannot be damaged | Side effects are possible, intended or not |
What Python does
Python does not let you choose. Every argument is passed as a reference to an object, and what you see depends on what the subroutine does with it:
def nudge(cm):
cm = cm + 5 # makes the local name refer to a new integer
print("inside:", cm)
def log_reading(readings, cm):
readings.append(cm) # changes the list the caller also refers to
step = 10
nudge(step)
print("after nudge:", step)
survey = []
log_reading(survey, 31.0)
log_reading(survey, 51.0)
print("after logging:", survey)
nudge behaves like passing by value: integers cannot be changed, so cm + 5 makes a new integer and points only the local name at it. log_reading behaves like passing by reference: append changes the one list that both readings and survey refer to, just as the two names did in lesson A1.1. Assigning a new list, readings = [99], would not affect survey, because that only moves the local name.
So in Python, to change a caller's number, return the new value and let the caller assign it. To fill in a list or change an object, pass it in and change it; there is no need to return it. If a subroutine must not change a list it is given, pass it a copy, list(survey).
Task: survey by reference
Write two subroutines.
- The procedure
survey(readings)takes an empty listreadings. It takes eight distance readings, turning right 45 degrees after each one, so the robot ends facing where it started, and appends each reading toreadings. It returns nothing, and the main program calls it on a line of its own,survey(readings). - The function
nearest(readings)takes a list of real numbers and returns two values: the index of the smallest reading (the first one if two are equal) and that reading. Find it with a loop; do not usemin,indexorglobal.
Then print nearest: <cm> cm at <degrees> degrees, where the degrees are the index times 45, turn right by that many degrees to face the nearest wall, and play one note.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def survey(readings):
pass
def nearest(readings):
return 0, 0.0
readings = []
survey(readings)
index, cm = nearest(readings)
print(f"nearest: {cm} cm at {index * 45} degrees")
Challenges
- Write
survey_copy()that makes its own list and returns it. Which version is easier to test, and why? - Inside
survey, addreadings = []as the first line. What does the main program see now, and why? - Write
nearest_two(readings)that returns the indexes of the two smallest readings.