Other Ways to open this chapter: JupyterLite | Colab | Read Only | Download

Ignore this cell — used when running JupyterLite.

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/porttack/working-in-python/raw/v3/working_in_python.py');
download('https://github.com/AllenDowney/ThinkPython/raw/v3/diagram.py');
download('https://github.com/ramalho/jupyturtle/releases/download/2024-03/jupyturtle.py');

import working_in_python

%load_ext autoreload
%autoreload 2

working_in_python.check_for_update(
    "jupyterlite-1c4bc7016f", "Chapter04-Functions-and-Interfaces.ipynb"
)

4. Functions and Interfaces#

This chapter introduces a module called jupyturtle, which lets you create simple drawings by giving instructions to an imaginary turtle. We will use this module to write functions that draw squares, polygons, and circles – and to demonstrate interface design, which is a way of designing functions that work together.

4.1. The jupyturtle module#

To use the jupyturtle module, we can import it like this.

import jupyturtle

Now we can use the functions defined in the module, like make_turtle and forward.

jupyturtle.make_turtle()
jupyturtle.forward(100)

make_turtle creates a canvas, which is a space on the screen where we can draw, and a turtle, which is represented by a circular shell and a triangular head. The circle shows the location of the turtle and the triangle indicates the direction it is facing.

forward moves the turtle a given distance in the direction it’s facing, drawing a line segment along the way. The distance is in arbitrary units – the actual size depends on your computer’s screen.

We will use functions defined in the jupyturtle module many times, so it would be nice if we did not have to write the name of the module every time. That’s possible if we import the module like this.

from jupyturtle import make_turtle, forward

This version of the import statement imports make_turtle and forward from the jupyturtle module so we can call them like this.

make_turtle()
forward(100)

jupyturtle provides two other functions we’ll use, called left and right. We’ll import them like this.

from jupyturtle import left, right

left causes the turtle to turn left. It takes one argument, which is the angle of the turn in degrees. For example, we can make a 90 degree left turn like this.

make_turtle()
forward(50)
left(90)
forward(50)

This program moves the turtle east and then north, leaving two line segments behind. Before you go on, see if you can modify the previous program to make a square.

4.2. Making a square#

Here’s one way to make a square.

make_turtle()

forward(50)
left(90)

forward(50)
left(90)

forward(50)
left(90)

forward(50)
left(90)

Because this program repeats the same pair of lines four times, we can do the same thing more concisely with a for loop.

make_turtle()
for i in range(4):
    forward(50)
    left(90)

4.3. Encapsulation and generalization#

Let’s take the square-drawing code from the previous section and put it in a function called square.

def square():
    for i in range(4):
        forward(50)
        left(90)

Now we can call the function like this.

make_turtle()
square()

Wrapping a piece of code up in a function is called encapsulation. One of the benefits of encapsulation is that it attaches a name to the code, which serves as a kind of documentation. Another advantage is that if you re-use the code, it is more concise to call a function twice than to copy and paste the body!

In the current version, the size of the square is always 50. If we want to draw squares with different sizes, we can take the length of the sides as a parameter.

def square(length):
    for i in range(4):
        forward(length)
        left(90)

Now we can draw squares with different sizes.

make_turtle()
square(30)
square(60)

Adding a parameter to a function is called generalization because it makes the function more general: with the previous version, the square is always the same size; with this version it can be any size.

If we add another parameter, we can make it even more general. The following function draws regular polygons with a given number of sides.

def polygon(n, length):
    angle = 360 / n
    for i in range(n):
        forward(length)
        left(angle)

In a regular polygon with n sides, the angle between adjacent sides is 360 / n degrees.

The following example draws a 7-sided polygon with side length 30.

make_turtle()
polygon(7, 30)

When a function has more than a few numeric arguments, it is easy to forget what they are, or what order they should be in. It can be a good idea to include the names of the parameters in the argument list.

make_turtle()
polygon(n=7, length=30)

These are sometimes called “named arguments” because they include the parameter names. But in Python they are more often called keyword arguments (not to be confused with Python keywords like for and def).

This use of the assignment operator, =, is a reminder about how arguments and parameters work – when you call a function, the arguments are assigned to the parameters.

4.4. Approximating a circle#

Now suppose we want to draw a circle. We can do that, approximately, by drawing a polygon with a large number of sides, so each side is small enough that it’s hard to see. Here is a function that uses polygon to draw a 30-sided polygon that approximates a circle.

import math

def circle(radius):
    circumference = 2 * math.pi * radius
    n = 30
    length = circumference / n
    polygon(n, length)

circle takes the radius of the the circle as a parameter. It computes circumference, which is the circumference of a circle with the given radius. n is the number of sides, so circumference / n is the length of each side.

This function might take a long time to run. We can speed it up by calling make_turtle with a keyword argument called delay that sets the time, in seconds, the turtle waits after each step. The default value is 0.2 seconds – if we set it to 0.02 it runs about 10 times faster.

make_turtle(delay=0.02)
circle(30)

A limitation of this solution is that n is a constant, which means that for very big circles, the sides are too long, and for small circles, we waste time drawing very short sides. One option is to generalize the function by taking n as a parameter. But let’s keep it simple for now.

4.5. Refactoring#

Now let’s write a more general version of circle, called arc, that takes a second parameter, angle, and draws an arc of a circle that spans the given angle. For example, if angle is 360 degrees, it draws a complete circle. If angle is 180 degrees, it draws a half circle.

To write circle, we were able to reuse polygon, because a many-sided polygon is a good approximation of a circle. But we can’t use polygon to write arc.

Instead, we’ll create the more general version of polygon, called polyline.

def polyline(n, length, angle):
    for i in range(n):
        forward(length)
        left(angle)

polyline takes as parameters the number of line segments to draw, n, the length of the segments, length, and the angle between them, angle.

Now we can rewrite polygon to use polyline.

def polygon(n, length):
    angle = 360.0 / n
    polyline(n, length, angle)

And we can use polyline to write arc.

def arc(radius, angle):
    arc_length = 2 * math.pi * radius * angle / 360
    n = 30
    length = arc_length / n
    step_angle = angle / n
    polyline(n, length, step_angle)

arc is similar to circle, except that it computes arc_length, which is a fraction of the circumference of a circle.

Finally, we can rewrite circle to use arc.

def circle(radius):
    arc(radius,  360)

To check that these functions work as expected, we’ll use them to draw something like a snail. With delay=0, the turtle runs as fast as possible.

make_turtle(delay=0)
polygon(n=20, length=9)
arc(radius=70, angle=70)
circle(radius=10)

In this example, we started with working code and reorganized it with different functions. Changes like this, which improve the code without changing its behavior, are called refactoring.

If we had planned ahead, we might have written polyline first and avoided refactoring, but often you don’t know enough at the beginning of a project to design all the functions. Once you start coding, you understand the problem better. Sometimes refactoring is a sign that you have learned something.

4.6. Stack diagram#

When we call circle, it calls arc, which calls polyline. We can use a stack diagram to show this sequence of function calls and the parameters for each one.

from diagram import make_binding, make_frame, Frame, Stack

frame1 = make_frame(dict(radius=30), name='circle', loc='left')

frame2 = make_frame(dict(radius=30, angle=360), name='arc', loc='left', dx=1.1)

frame3 = make_frame(dict(n=60, length=3.04, angle=5.8), 
                    name='polyline', loc='left', dx=1.1, offsetx=-0.27)

stack = Stack([frame1, frame2, frame3], dy=-0.4)
from diagram import diagram, adjust

width, height, x, y = [3.58, 1.31, 0.98, 1.06]
ax = diagram(width, height)
bbox = stack.draw(ax, x, y)
#adjust(x, y, bbox)

Notice that the value of angle in polyline is different from the value of angle in arc. Parameters are local, which means you can use the same parameter name in different functions; it’s a different variable in each function, and it can refer to a different value.

4.7. A development plan#

A development plan is a process for writing programs. The process we used in this chapter is “encapsulation and generalization”. The steps of this process are:

  1. Start by writing a small program with no function definitions.

  2. Once you get the program working, identify a coherent piece of it, encapsulate the piece in a function and give it a name.

  3. Generalize the function by adding appropriate parameters.

  4. Repeat Steps 1 to 3 until you have a set of working functions.

  5. Look for opportunities to improve the program by refactoring. For example, if you have similar code in several places, consider factoring it into an appropriately general function.

This process has some drawbacks – we will see alternatives later – but it can be useful if you don’t know ahead of time how to divide the program into functions. This approach lets you design as you go along.

The design of a function has two parts:

  • The interface is how the function is used, including its name, the parameters it takes and what the function is supposed to do.

  • The implementation is how the function does what it’s supposed to do.

For example, here’s the first version of circle we wrote, which uses polygon.

def circle(radius):
    circumference = 2 * math.pi * radius
    n = 30
    length = circumference / n
    polygon(n, length)

And here’s the refactored version that uses arc.

def circle(radius):
    arc(radius,  360)

These two functions have the same interface – they take the same parameters and do the same thing – but they have different implementations.

4.8. Docstrings#

A docstring is a string at the beginning of a function that explains the interface (“doc” is short for “documentation”). Here is an example:

def polyline(n, length, angle):
    """Draws line segments with the given length and angle between them.
    
    n: integer number of line segments
    length: length of the line segments
    angle: angle between segments (in degrees)
    """    
    for i in range(n):
        forward(length)
        left(angle)

By convention, docstrings are triple-quoted strings, also known as multiline strings because the triple quotes allow the string to span more than one line.

A docstring should:

  • Explain concisely what the function does, without getting into the details of how it works,

  • Explain what effect each parameter has on the behavior of the function, and

  • Indicate what type each parameter should be, if it is not obvious.

Writing this kind of documentation is an important part of interface design. A well-designed interface should be simple to explain; if you have a hard time explaining one of your functions, maybe the interface could be improved.

4.9. Debugging#

An interface is like a contract between a function and a caller. The caller agrees to provide certain arguments and the function agrees to do certain work.

For example, polyline requires three arguments: n has to be an integer; length should be a positive number; and angle has to be a number, which is understood to be in degrees.

These requirements are called preconditions because they are supposed to be true before the function starts executing. Conversely, conditions at the end of the function are postconditions.

Postconditions include the intended effect of the function (like drawing line segments) and any side effects (like moving the turtle or making other changes).

Preconditions are the responsibility of the caller. If the caller violates a precondition and the function doesn’t work correctly, the bug is in the caller, not the function.

If the preconditions are satisfied and the postconditions are not, the bug is in the function. If your pre- and postconditions are clear, they can help with debugging.

4.10. Glossary#

interface design: A process for designing the interface of a function, which includes the parameters it should take.

canvas: A window used to display graphical elements including lines, circles, rectangles, and other shapes.

encapsulation: The process of transforming a sequence of statements into a function definition.

generalization: The process of replacing something unnecessarily specific (like a number) with something appropriately general (like a variable or parameter).

keyword argument: An argument that includes the name of the parameter.

refactoring: The process of modifying a working program to improve function interfaces and other qualities of the code.

development plan: A process for writing programs.

docstring: A string that appears at the top of a function definition to document the function’s interface.

multiline string: A string enclosed in triple quotes that can span more than one line of a program.

precondition: A requirement that should be satisfied by the caller before a function starts.

postcondition: A requirement that should be satisfied by the function before it ends.

See also: the full vocabulary glossary collects every term in this book, alphabetized, alongside the complete AP CSP exam vocabulary list.

4.11. Exercises#

# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.

%xmode Verbose

For the exercises below, there are a few more turtle functions you might want to use.

  • penup lifts the turtle’s imaginary pen so it doesn’t leave a trail when it moves.

  • pendown puts the pen back down.

The following function uses penup and pendown to move the turtle without leaving a trail.

from jupyturtle import penup, pendown

def jump(length):
    """Move forward length units without leaving a trail.
    
    Postcondition: Leaves the pen down.
    """
    penup()
    forward(length)
    pendown()

4.11.1. Exercise#

Write a function called rectangle that draws a rectangle with given side lengths. For example, here’s a rectangle that’s 80 units wide and 40 units tall.

You can use the following code to test your function.

make_turtle()
rectangle(80, 40)

4.11.2. Exercise#

Write a function called rhombus that draws a rhombus with a given side length and a given interior angle. For example, here’s a rhombus with side length 50 and an interior angle of 60 degrees.

You can use the following code to test your function.

make_turtle()
rhombus(50, 60)

4.11.3. Exercise#

Now write a more general function called parallelogram that draws a quadrilateral with parallel sides. Then rewrite rectangle and rhombus to use parallelogram.

You can use the following code to test your functions.

make_turtle(width=400)
jump(-120)

rectangle(80, 40)
jump(100)
rhombus(50, 60)
jump(80)
parallelogram(80, 50, 60)

4.11.4. Exercise#

Write an appropriately general set of functions that can draw shapes like this.

Hint: Write a function called triangle that draws one triangular segment, and then a function called draw_pie that uses triangle.

This one is optional – try it if you have time, but it isn’t expected.

You can use the following code to test your functions.

turtle = make_turtle(delay=0)
jump(-80)

size = 40
draw_pie(5, size)
jump(2*size)
draw_pie(6, size)
jump(2*size)
draw_pie(7, size)

4.11.5. Exercise#

Write an appropriately general set of functions that can draw flowers like this.

Hint: Use arc to write a function called petal that draws one flower petal.

This one is optional – try it if you have time, but it isn’t expected.

You can use the following code to test your functions.

Because the solution draws a lot of small line segments, it tends to slow down as it runs. To avoid that, you can add the keyword argument auto_render=False to avoid drawing after every step, and then call the render function at the end to show the result.

While you are debugging, you might want to remove auto_render=False.

from jupyturtle import render

turtle = make_turtle(auto_render=False)

jump(-60)
n = 7
radius = 60
angle = 60
flower(n, radius, angle)

jump(120)
n = 9
radius = 40
angle = 85
flower(n, radius, angle)

render()

4.12. Homework#

Do either Exercise 1 or Exercise 2, not both. They’re interchangeable, so pick whichever appeals to you. Doing both is fine, but the second one earns no additional credit. Required work is five exercises (whichever of 1/2 you pick, plus Exercises 3-6), about 38 minutes total. The time check below must be filled in. The spiral at the end is extra credit, the only extra credit in this chapter, and it never substitutes for a required exercise.

Note: the turtle-drawing exercises above (rectangle, rhombus, parallelogram, pie, flower) are practice. They aren’t graded. This is the graded homework.

4.12.1. Exercise 1: draw your initials (do 1 or 2)#

Using forward, left, right, and jump, draw your first and last initials with the turtle. Build each letter as its own function. For example, if your initials are A and T, write draw_a() and draw_t(), then a function called draw_initials() that calls them with a jump in between so they don’t run together.

If one of your initials has a curve in it (like C, O, S, G, or R), you don’t need a perfect curve. Approximate it with several short straight segments that turn a little each time, the same idea the chapter used to approximate a circle with a polygon.

make_turtle()
# Solution goes here

4.12.2. Exercise 2: draw a house from parts (do 1 or 2)#

Using forward, left, right, and jump, write draw_wall(), draw_roof(), and draw_door() as three separate functions, then a function called draw_house() that calls all three, using jump to position each piece so they end up in the right places relative to each other.

If you want to add a round window, you don’t need a perfect curve. Approximate it with several short straight segments, the same idea the chapter used to approximate a circle with a polygon.

make_turtle()
# Solution goes here

4.12.3. Exercise 3: docstrings#

Go back to the function you wrote for Exercise 1 or 2 (draw_initials() or draw_house(), whichever you chose) and add a docstring to it, following the chapter’s polyline example: explain in your own words what the function does.

Re-run that cell so the docstring takes effect, then run help(draw_initials) or help(draw_house) (whichever applies) and confirm your docstring appears in the output.

4.12.4. Exercise 4: pinwheel#

You’ll need a square function that takes a size parameter, the same one from the chapter. Define it again here, then write a function called pinwheel that draws several overlapping squares, each rotated a little further than the last, without lifting the pen between them:

def pinwheel(n_shapes, size):
    for i in range(n_shapes):
        square(size)
        left(360 / n_shapes)

Try a few different values of n_shapes (6, then 12, then 36) and see what patterns emerge. Nothing here is new syntax. The surprise is how much visual complexity comes from just repeating and rotating a shape you already know how to draw.

4.12.5. Exercise 5: interface vs. implementation#

The chapter shows two versions of circle:

def circle(radius):
    circumference = 2 * math.pi * radius
    n = 30
    length = circumference / n
    polygon(n, length)

and

def circle(radius):
    arc(radius, 360)

These have the same interface but different implementations. In your own words, what does that mean?

Answer in the markdown cell below.

Type your answer here.

4.12.6. Exercise 6: reflection#

The chapter’s development plan centers on three moves: encapsulation (wrapping code in a function), generalization (adding parameters), and refactoring (reorganizing working code without changing what it does). Which of these did you find yourself doing naturally while working through this chapter’s exercises, and which felt unfamiliar?

Answer in the markdown cell below, the same way you did in chap01, chap02, and chap03.

Type your answer here.

4.12.7. Time check#

This is graded on being filled in, not on the numbers. There’s no right answer, and low numbers don’t score better. If this chapter took you three hours, I need to know that. These are estimates: nobody expects you to have timed yourself, so a rough number is exactly what’s wanted.

# Fill this in before you submit. Rough estimates are fine, and low numbers
# don't score better. Don't count time on the extra-credit spiral below.
# chapter_minutes -> reading the chapter and its practice exercises
# extra_exercises_minutes -> the six numbered exercises above
# longest -> which exercise took longest, e.g. 1
chapter_minutes = 0
extra_exercises_minutes = 0
longest = 0

from working_in_python import time_check
time_check(chapter_minutes, extra_exercises_minutes, longest)

4.12.8. Spiral (extra credit)#

Write a function called spiral that draws a spiral using forward and left, where each segment is slightly longer than the one before. The loop body is nearly identical to polygon from the chapter. The only difference is that the length changes each time through.

Pick your own parameters (number of segments, starting length, growth per step, turn angle) and try a few combinations.

This is extra credit. It does not replace any required exercise.

make_turtle()
# Solution goes here

4.12.9. Finished? Copy your work#

This isn’t part of Exercise 6. It’s a tool. Run the cell below to copy this notebook (including anything you’ve run) so you can paste it into a document.

working_in_python.show_copy_notebook_button()

Working in Python — modified by Eric Brown for a high school Computer Science Principles class. Source and modifications: github.com/porttack/working-in-python


Think Python: 3rd Edition

Copyright 2024 Allen B. Downey

Code license: MIT License

Text license: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International


4.13. Standards alignment#

AP CSP: 3.13 Developing Procedures (Big Idea 3, 30–35% of the exam) and 1.3 Program Design and Development (Big Idea 1, 10–13% — chapter 4 is this topic’s only carrier in the book). Also 1.4 Identifying and Correcting Errors and 3.14 Libraries, headers only. California 9-12: 9-12.AP.16, 9-12.AP.17, 9-12.AP.20, 9-12.AP.22 CSTA 2026: HS-PRO-PD-12, HS-ALG-PS-02. Also HS-PRO-PD-13 and HS-PRO-RD-17, headers only. CA CTE (ICT): C4.9 (Pathway C), 5.9, 5.10 (Anchor Standard 5, Problem Solving). Also C4.11 and C5.6, headers only.

This chapter turns decomposition into a design habit: wrapping code in functions, generalizing them with parameters, and refactoring circle into arc and polyline once a better shape emerges. The development plan and docstrings make that design process explicit and documented — the clearest interface-versus-implementation example in the book so far, the same idea California, CSTA, and the ICT standards each name in their own words. Debugging returns as preconditions and postconditions, the same recurring thread as every other chapter.

Indexing: this chapter’s keyword arguments (polygon(n=7, length=30)) have no equivalent in the exam’s pseudocode, which calls every procedure positionally.