4b. Prof. Rosenthal’s Review#
A review of Chapter 4: Functions and Interfaces, for students who have already studied the chapter. Use the two lists under Concepts covered as a checklist (click a list’s title to hide it), then work through the Questions, which go from easy to hard. Each question has a suggested answer under Answer. Try the question first, then open the Answer.
Concepts covered#
Python Syntax and Semantics
Importing
import MODULE_NAMEmakes a module’s functions available asMODULE_NAME.FUNCTION_NAME(...). For example, afterimport jupyturtleyou can calljupyturtle.forward(100).from MODULE_NAME import NAME1, NAME2makes only the listed functions available, by their names alone. For example, afterfrom jupyturtle import forwardyou can callforward(100). It does not define the nameMODULE_NAME.import mathmakesmath.pi(the number pi, 3.14159…) available.
The jupyturtle module (turtle graphics in Jupyter and Colab)
make_turtle()starts a new drawing, with the turtle in the middle, facing east (to the right).make_turtletakes optional keyword arguments:delay=SECONDS(pause after each step; default0.2;0is fastest),width=PIXELS,height=PIXELS, andauto_render=False(show the drawing only whenrender()is called).forward(DISTANCE)moves the turtle and draws a line. A negative distance moves it backward.left(DEGREES)andright(DEGREES)turn the turtle.penup()stops drawing as the turtle moves;pendown()starts drawing again.
for loops and range
This repeats the indented code SOME_INTEGER times:
for i in range(SOME_INTEGER): SOME_CODE SOME_MORE_CODE
For example, this prints
hellothree times:for i in range(3): print('hello')
range(SOME_INTEGER)is shorthand for the integers0, 1, 2, ...up to SOME_INTEGER minus 1. The loop variable (i) takes these values in turn: infor i in range(4):,iis0, then1, then2, then3.rangeneeds an integer:range(4.5)is aTypeError.range(0)andrange(-4)produce no values, so the loop body runs zero times.
Functions with parameters
This defines a function with two parameters:
def FUNCTION_NAME(PARAMETER1, PARAMETER2): BODY
The body runs only when the function is called.
FUNCTION_NAME(ARGUMENT1, ARGUMENT2)calls it: the arguments are assigned to the parameters, in order.A function can call other functions. Each call finishes before the next statement runs.
Keyword arguments
FUNCTION_NAME(PARAMETER_NAME=VALUE)passes an argument by naming its parameter, as inpolygon(n=7, length=30). With keyword arguments, the order does not matter.Positional arguments (without names) must come first:
polygon(n=7, 30)is aSyntaxError.Each parameter gets one value:
polygon(30, n=7)is aTypeError. So is a name that is not a parameter:polygon(7, size=30).
Local variables
Parameters, and variables assigned inside a function, are local to one call of the function. The same name in two functions means two different variables.
Assigning a new value to a parameter does not change the caller’s variable.
Division
/always produces a float:360 / 4is90.0.Dividing by zero, as in
360 / 0, is aZeroDivisionError.
Docstrings and multiline strings
"""TEXT"""(triple quotes) is a string that can span several lines.A string on the first line of a function’s body is the function’s docstring:
def FUNCTION_NAME(PARAMETERS): """WHAT THE FUNCTION DOES""" BODY
A docstring is not displayed when the function runs.
help(FUNCTION_NAME)displays it.
Definitions, etc.
Definitions
DEFINITION: Encapsulation is wrapping a sequence of statements in a function definition.
DEFINITION: Generalization is replacing something unnecessarily specific (such as a number) with something more general (such as a parameter).
DEFINITION: A keyword argument is an argument that includes the name of the parameter.
DEFINITION: Refactoring is changing working code to improve it (for example, its function interfaces) without changing what it does.
DEFINITION: The interface of a function is how it is used: its name, its parameters, and what it does.
DEFINITION: The implementation of a function is how it does what it does.
DEFINITION: Interface design is deciding on a function’s interface, including which parameters it takes.
DEFINITION: A docstring is a string at the beginning of a function that explains its interface.
DEFINITION: A multiline string is a string in triple quotes, which can span more than one line.
DEFINITION: A precondition is a requirement the caller must satisfy before a function starts.
DEFINITION: A postcondition is something a function guarantees when it ends: its intended effect and any side effects.
DEFINITION: A stack diagram shows the function calls in progress, one frame per call, with each call’s parameters and their values.
DEFINITION: A development plan is a process for writing programs.
Other ideas
Encapsulation gives code a name that documents it, and calling a function twice is shorter than copying its code.
Two functions can have the same interface and different implementations.
The “encapsulation and generalization” development plan:
Write a small program with no function definitions.
When it works, put a coherent piece of it in a function and give the function a name.
Generalize the function by adding parameters.
Repeat steps 1 to 3 until you have a set of working functions.
Look for chances to refactor, for example by replacing similar code in several places with one general function.
That plan is useful when you don’t know in advance how to divide a program into functions.
Refactoring is normal: often you don’t know enough at the start of a project to design all the functions.
A good docstring says what the function does (not how), what each parameter means, and each parameter’s type if it is not obvious. If a function is hard to explain, its interface may need improving.
An interface is like a contract: the caller provides certain arguments and the function does certain work.
If the caller breaks a precondition, the bug is in the caller. If the preconditions hold but a postcondition does not, the bug is in the function.
Questions#
Some questions draw pictures with jupyturtle, a small turtle-graphics module for Jupyter notebooks.
The turtle starts in the middle of the canvas, facing east (to the right).
make_turtle() starts a new drawing, forward(d) moves the turtle d units while drawing a line,
left(a) and right(a) turn it a degrees, and penup() and pendown() stop and restart the drawing.
The setup cell below downloads jupyturtle and imports the functions the questions use.
from os.path import basename, exists
def download(url):
filename = basename(url)
if not exists(filename):
from urllib.request import urlretrieve
local, _ = urlretrieve(url, filename)
print("Downloaded " + str(local))
return filename
download('https://github.com/ramalho/jupyturtle/releases/download/2024-03/jupyturtle.py');
import math
from jupyturtle import make_turtle, forward, left, right, penup, pendown
Question 1 (easy): what is displayed?#
Predict what this code displays, then open the Answer to check.
for n in range(3, 7):
print(n, 360 / n)
Answer
3 120.0
4 90.0
5 72.0
6 60.0
The / operator always produces a float, even when the division comes out even, so 360 / 4 is 90.0, not 90.
Each print displays its two values separated by a space.
By the way: 360 / n is the number of degrees a turtle must turn at each corner to draw a regular polygon
(all sides and angles equal) with n sides. Some of the later questions use this.
Question 2 (easy): what is drawn?#
Describe the drawing this code makes, then open the Answer to check.
make_turtle()
for i in range(3):
forward(60)
left(120)
Answer
An equilateral triangle (three equal sides) with sides of length 60:
The turtle starts facing east, so the first side goes to the right.
Each left(120) turns it counterclockwise, so the triangle is drawn above the starting line.
Three turns of 120 degrees add up to 360, a full turn, so the turtle ends where it started, facing east again.
Question 3 (easy): write a function#
Write a function called triangle that takes a parameter length and draws an equilateral triangle with that side length.
Start from this header (replace ... with the body):
def triangle(length):
...
Examples
make_turtle()
triangle(80)
make_turtle()
triangle(30)
Answer
Use a loop that draws a side and turns 120 degrees, three times, with the parameter as the side length:
from jupyturtle import forward, left
def triangle(length):
for i in range(3):
forward(length)
left(120)
Then try: make_turtle() and triangle(80).
Another approach: write a general polygon function first, then use it.
triangle works exactly the same way (same interface), with a different implementation:
from jupyturtle import forward, left
def polygon(n, length):
angle = 360 / n
for i in range(n):
forward(length)
left(angle)
def triangle(length):
polygon(3, length)
Question 4 (easy): what is displayed?#
def greet(name):
"""Print a greeting.
name: string
"""
print('Hello,', name)
greet('Ada')
Answer
Hello, Ada
The docstring documents the function. It is not displayed when the function runs.
Question 5 (easy): what is displayed?#
def describe(shape, sides):
print('A', shape, 'has', sides, 'sides')
describe('square', 4)
describe(sides=3, shape='triangle')
Answer
A square has 4 sides
A triangle has 3 sides
With keyword arguments, each argument is assigned to the parameter it names, so their order in the call does not matter.
Question 6 (medium): what is drawn?#
make_turtle()
for i in range(4):
forward(50)
right(90)
Answer
A square with sides of length 50, drawn clockwise, below the starting line:
The first side goes east. Turning right then points the turtle south, so the square goes downward.
With left(90) instead of right(90), the same square would be drawn above the starting line.
Question 7 (medium): what happens?#
Assume you have just restarted the notebook and run only the setup cells (at the start of the Questions), and then this code.
from jupyturtle import make_turtle, forward
make_turtle()
forward(50)
jupyturtle.left(90)
forward(50)
Answer
The turtle draws one line, 50 units to the east, and then Python stops with a NameError:
2 make_turtle()
3 forward(50)
----> 4 jupyturtle.left(90)
5 forward(50)
NameError: name 'jupyturtle' is not defined
from jupyturtle import make_turtle, forward defines the names make_turtle and forward, but not the name jupyturtle.
To call jupyturtle.left, you need import jupyturtle. Or import left as well and call left(90).
The last line, forward(50), never runs.
Question 8 (medium): what is displayed?#
def inner(angle):
angle = angle / 2
print('inner:', angle)
def outer(angle):
inner(angle)
print('outer:', angle)
outer(90)
Answer
inner: 45.0
outer: 90
Parameters are local.
angle in inner and angle in outer are two different variables, so changing one does not change the other.
outer’s angle is still the integer 90; inner’s is the float 45.0.
Question 9 (medium): what is displayed?#
def c(x):
print('c starts with', x)
def b(x):
print('b starts with', x)
c(x * 2)
print('b ends with', x)
def a(x):
print('a starts with', x)
b(x + 1)
print('a ends with', x)
a(3)
Answer
a starts with 3
b starts with 4
c starts with 8
b ends with 4
a ends with 3
When a function calls another, it waits for that call to finish before going on.
Each function has its own x, so after the calls return, b’s x is still 4 and a’s is still 3.
Question 10 (medium): write a function#
Write a function called star that takes a parameter length and draws a five-pointed star whose lines have that length.
Hint: the turtle draws 5 lines and turns the same angle after each one. In total it turns around twice (720 degrees).
Start from this header (replace ... with the body):
def star(length):
...
Examples
make_turtle()
star(100)
make_turtle()
star(50)
Answer
Each turn is 720 / 5 = 144 degrees:
from jupyturtle import forward, right
def star(length):
for i in range(5):
forward(length)
right(144)
Then try: make_turtle() and star(100).
Turning left(144) instead also draws a star, flipped upside down.
Another approach: a general function that draws n lines with a given turn between them, then star uses it:
from jupyturtle import forward, right
def polyline(n, length, angle):
for i in range(n):
forward(length)
right(angle)
def star(length):
polyline(5, length, 144)
Question 11 (medium): write a function#
Write a function called row_of_squares that takes n and length and draws n squares in a row, side by side and touching, each with side length length.
Run this cell to define square and jump.
def square(length):
for i in range(4):
forward(length)
left(90)
def jump(length):
"""Move forward length units without drawing."""
penup()
forward(length)
pendown()
Start from this header (replace ... with the body):
def row_of_squares(n, length):
...
Examples (jump(-150) first moves the turtle left, so the row fits on the canvas)
make_turtle(width=400)
jump(-150)
row_of_squares(5, 40)
make_turtle()
jump(-120)
row_of_squares(3, 60)
Answer
square leaves the turtle where it started, facing east, so after each square, jump to the start of the next one:
def row_of_squares(n, length):
for i in range(n):
square(length)
jump(length)
Then try: make_turtle(width=400), jump(-150) and row_of_squares(5, 40).
Using forward(length) instead of jump(length) draws the same picture, because the turtle moves along a bottom edge it has just drawn.
jump makes the intention clearer: move without drawing.
Question 12 (medium): write a function with a docstring#
Write a function called staircase that takes steps and size and draws a staircase going up and to the right:
for each step, it goes up size units and then right size units.
Give it a docstring that explains its interface (what it does and what each parameter means).
Start from this header (replace ... with the body):
def staircase(steps, size):
...
Examples
make_turtle()
staircase(4, 15)
make_turtle()
staircase(2, 30)
help displays a function’s docstring. Yours will have your own wording; for example:
help(staircase)
Help on function staircase in module __main__:
staircase(steps, size)
Draw a staircase going up and to the right.
steps: number of steps (a positive integer)
size: height and depth of each step
Answer
from jupyturtle import forward, left, right
def staircase(steps, size):
"""Draw a staircase going up and to the right.
steps: number of steps (a positive integer)
size: height and depth of each step
"""
for i in range(steps):
left(90)
forward(size)
right(90)
forward(size)
Then try: make_turtle() and staircase(4, 15).
The docstring says what the function does, not how, and what each parameter means. The turtle starts facing east, so each step turns left (to face north), goes up, turns right (back to east) and goes forward.
Question 13 (hard): what is displayed?#
def square_txt(length):
print('square', length)
def growing(n, length):
for i in range(n):
square_txt(length)
length = length * 2
print('growing ends with', length)
length = 5
growing(3, length)
print('outside:', length)
Answer
square 5
square 10
square 20
growing ends with 40
outside: 5
Inside growing, length is a local variable.
Assigning to it changes the value used in the next time through the loop, and after the loop it has been doubled 3 times (5, 10, 20, 40).
The length outside the function is a different variable, so it is still 5.
Question 14 (hard): preconditions#
polygon’s docstring states its preconditions.
Run this cell to define polygon.
def polygon(n, length):
"""Draw a regular polygon.
Preconditions: n is a positive integer; length is a positive number.
"""
angle = 360 / n
for i in range(n):
forward(length)
left(angle)
Two correct ways to use it:
make_turtle()
polygon(4, 40)
make_turtle()
polygon(n=6, length=25)
For each of the three parts, predict what happens.
Then say whether the bug is in the caller or in polygon.
Part a
make_turtle()
polygon(4.5, 20)
Part b
make_turtle()
polygon(0, 20)
Part c
make_turtle()
polygon(-4, 20)
Answer
Part a: angle = 360 / 4.5 works (it is 80.0), but range(4.5) fails, so nothing is drawn (a TypeError):
1 make_turtle()
----> 2 polygon(4.5, 20)
In polygon(n, length):
5 """
6 angle = 360 / n
----> 7 for i in range(n):
8 forward(length)
9 left(angle)
TypeError: 'float' object cannot be interpreted as an integer
Part b: 360 / 0 fails before anything is drawn (a ZeroDivisionError):
1 make_turtle()
----> 2 polygon(0, 20)
In polygon(n, length):
4 Preconditions: n is a positive integer; length is a positive number.
5 """
----> 6 angle = 360 / n
7 for i in range(n):
8 forward(length)
ZeroDivisionError: division by zero
Part c: No error, and nothing is drawn: angle is -90.0 and range(-4) is empty, so the loop body never runs.
This is the most dangerous case, because nothing tells you something is wrong.
In all three parts the caller broke a precondition, so the bug is in the caller, not in polygon.
Question 15 (hard): keyword arguments#
Run this cell to define describe.
def describe(shape, sides):
print('A', shape, 'has', sides, 'sides')
Two correct ways to call it:
describe('hexagon', 6)
A hexagon has 6 sides
describe(sides=5, shape='pentagon')
A pentagon has 5 sides
Each of the next three calls is wrong in a different way. Predict the error for each, then open the Answer to check.
Part a
describe(shape='square', 4)
Part b
describe('square', shape='box')
Part c
describe('square', size=4)
Answer
Part a: Positional arguments must come before keyword arguments, so Python rejects the line before running it (a SyntaxError):
describe(shape='square', 4)
^
SyntaxError: positional argument follows keyword argument
Part b: 'square' is assigned to the first parameter, shape, and then shape='box' tries to assign shape again (a TypeError):
----> 1 describe('square', shape='box')
TypeError: describe() got multiple values for argument 'shape'
Part c: describe has no parameter called size. A keyword must be one of the parameter names (shape or sides) (a TypeError):
----> 1 describe('square', size=4)
TypeError: describe() got an unexpected keyword argument 'size'. Did you mean 'sides'?
(Python versions before 3.13 leave out the suggestion “Did you mean ‘sides’?”.)
Some correct calls: describe('square', 4), describe(shape='square', sides=4), describe(sides=4, shape='square') and describe('square', sides=4).
Question 16 (hard): write a function#
Write a function called spiral that takes n, length, angle and increase.
It draws n line segments, turning left angle degrees after each one.
The first segment has length length, and each segment is increase units longer than the one before.
Start from this header (replace ... with the body):
def spiral(n, length, angle, increase):
...
Examples
make_turtle(delay=0)
spiral(40, 2, 30, 1)
make_turtle(delay=0)
spiral(20, 5, 90, 5)
Answer
length is a local variable, so the function can change it as it goes:
from jupyturtle import forward, left
def spiral(n, length, angle, increase):
for i in range(n):
forward(length)
left(angle)
length = length + increase
Then try: make_turtle(delay=0) and spiral(40, 2, 30, 1).
Another approach computes each segment’s length from the loop variable, without changing length:
from jupyturtle import forward, left
def spiral(n, length, angle, increase):
for i in range(n):
forward(length + i * increase)
left(angle)
Question 17 (hard): write a function#
Write a function called grid that takes rows, cols and size and draws a grid of squares, rows high and cols wide, each with side length size.
The turtle starts at the bottom-left corner of the grid, facing east.
Hint: to get from the end of one row to the start of the next, use jump (with a negative length to go back), left and right.
Start from this header (replace ... with the body):
def grid(rows, cols, size):
...
Examples
make_turtle(height=220, delay=0)
jump(-75)
grid(3, 5, 30)
make_turtle(delay=0)
jump(-60)
grid(2, 4, 30)
This question uses jump and square from Question 11.
Answer
Put one row in a function first, then call it once for each row:
from jupyturtle import left, right
def row(cols, size):
for i in range(cols):
square(size)
jump(size)
def grid(rows, cols, size):
for i in range(rows):
row(cols, size)
jump(-cols * size) # back to the start of this row
left(90)
jump(size) # up to the start of the next row
right(90)
Then try: make_turtle(height=220, delay=0), jump(-75) and grid(3, 5, 30).
The same thing with a loop inside a loop instead of the row function:
from jupyturtle import left, right
def grid(rows, cols, size):
for i in range(rows):
for j in range(cols):
square(size)
jump(size)
jump(-cols * size)
left(90)
jump(size)
right(90)
The first version is easier to read and test, because each function does one thing and has a name that says what.
Question 18 (hard): refactor#
These two functions work, but they repeat almost all of their code.
def triangle_row(n, length):
for i in range(n):
for j in range(3):
forward(length)
left(120)
jump(length)
def square_row(n, length):
for i in range(n):
for j in range(4):
forward(length)
left(90)
jump(length)
Refactor them: write one general function, polygon_row(sides, n, length), that draws a row of n regular polygons with sides sides.
Then rewrite triangle_row and square_row to use it, without changing how they are called or what they draw.
Start from this header (replace ... with the body):
def polygon_row(sides, n, length):
...
def triangle_row(n, length):
...
def square_row(n, length):
...
Examples (they draw the same pictures before and after the refactoring)
make_turtle(delay=0)
jump(-80)
triangle_row(4, 40)
make_turtle(delay=0)
jump(-60)
square_row(3, 40)
This question uses jump from Question 11, and polygon from Question 14.
Answer
def polygon_row(sides, n, length):
for i in range(n):
polygon(sides, length)
jump(length)
def triangle_row(n, length):
polygon_row(3, n, length)
def square_row(n, length):
polygon_row(4, n, length)
Then try: make_turtle(delay=0), jump(-80) and triangle_row(4, 40).
triangle_row and square_row keep the same interface (name, parameters, what they draw) with a new implementation,
so any code that calls them still works.
The inner loops became calls to polygon, which already does that job.
Question 19 (hard): generalize a function#
This function draws an arc (part of a circle) with the given radius, spanning angle degrees, as 30 short straight lines:
def arc(radius, angle):
arc_length = 2 * math.pi * radius * angle / 360
n = 30
length = arc_length / n
step_angle = angle / n
for i in range(n):
forward(length)
left(step_angle)
It always uses 30 lines: for a big circle each line is long and the circle looks jagged, and for a tiny one most lines are too short to see.
Write
arc_n(radius, angle, n), which does the same but drawsnlines.Write
circle(radius), which usesarc_nto draw a full circle, choosingnfrom the radius so that each line is about3units long, with at least10lines. (int()converts a float to an integer by dropping the fraction.)
Start from this header (replace ... with the body):
def arc_n(radius, angle, n):
...
def circle(radius):
...
Examples
make_turtle(delay=0)
arc_n(30, 180, 3)
make_turtle(delay=0)
arc_n(30, 180, 30)
make_turtle(delay=0)
circle(30)
Answer
import math
from jupyturtle import forward, left
def arc_n(radius, angle, n):
arc_length = 2 * math.pi * radius * angle / 360
length = arc_length / n
step_angle = angle / n
for i in range(n):
forward(length)
left(step_angle)
def circle(radius):
circumference = 2 * math.pi * radius
n = int(circumference / 3) + 10
arc_n(radius, 360, n)
Then try: make_turtle(delay=0) and arc_n(30, 180, 3).
For radius 30 the circumference is about 188, so n is 62 + 10 = 72 lines; for radius 300 it would be 628 + 10 = 638.
Adding 10 guarantees at least 10 lines, even for a tiny circle.
Another way to say “at least 10” is max(10, int(circumference / 3)), if you know the max function.
Summary and questions by Prof. Y. Rosenthal, based on Think Python: 3rd Edition by Allen B. Downey. Text license: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International