Chapter 4 · pp. 51–60
Case Study: Interface Design
This chapter shows how to design several functions so that they work well together. It uses the turtle module, which draws pictures by moving a small pen-carrying "turtle" around a window. Along the way you will learn loops, how to make functions more flexible, how to clean up working code, and how to document and debug function interfaces.
Drawing with the turtle module
A method is a function that belongs to a particular object. You call it with dot notation, such as bob.fd(100). Calling a method is like making a request: you ask the object bob to move forward. (pp. 52, 59)
To draw with turtles, follow these steps:
- Import the module with
import turtle.
- Create a Turtle object, for example
bob = turtle.Turtle(). The module name is lowercase, and the function Turtle starts with a capital letter.
- Call movement methods on the object.
- End the program with
turtle.mainloop(). This keeps the window open and waiting until the user closes it. (pp. 51–52)
The main methods are:
fd(d) moves forward and bk(d) moves backward. The distance is in pixels, so the real size depends on your screen.
lt(a) turns left and rt(a) turns right. The angle is in degrees.
pd() puts the pen down, so the turtle leaves a trail. pu() lifts the pen up, so it moves without drawing. (p. 52)
Example: after creating bob, the lines bob.fd(100), bob.lt(90), bob.fd(100) draw two sides of a right angle. The turtle starts facing east, goes 100 pixels, turns left, and then goes 100 pixels north. (p. 52)
Repeating with a for loop
A square needs the same two steps four times: move forward, then turn 90 degrees. Writing them out four times is long. A for loop repeats an indented body a set number of times:
for i in range(4):
bob.fd(100)
bob.lt(90)
The header ends with a colon, and the body is indented, just like a function definition. The body can hold any number of statements. Execution runs through the body and then loops back to the top, which is why this is called a loop. Here the body runs 4 times. (pp. 52–53)
This version turns one extra time after the last side. That wastes a little time, but it keeps the code simple because every pass does the same thing. It also leaves the turtle at its starting point, facing its starting direction. (p. 53)
Encapsulation and generalization
Encapsulation means wrapping code in a function so it has a name. For the square:
def square(t):
for i in range(4):
t.fd(100)
t.lt(90)
The name works as documentation, since a reader sees "square" and knows what happens. Reuse is also shorter: calling square twice beats pasting the body twice. The parameter is called t instead of bob because any turtle can be passed in. If you make alice = turtle.Turtle(), then square(alice) draws with her. (p. 54)
Generalization means replacing something too specific, such as a fixed number, with a parameter. Adding length gives square(t, length), which draws squares of any size. Generalizing again gives polygon(t, n, length), which draws any regular polygon. For example, polygon(bob, 7, 70) draws a 7-sided shape with sides of 70 pixels. (pp. 54–55)
The key fact is that the exterior angle of a regular polygon with n sides is
n360 degrees.
The turtle turns by this angle after each side. For n=4 it turns 90 degrees, which matches the square. (pp. 54–55)
Keyword arguments put the parameter name in the call: polygon(bob, n=7, length=70). They help when a function has several numeric parameters whose order is easy to forget. They also remind you that arguments are assigned to parameters. Do not confuse them with Python keywords like while and def. (p. 55)
Designing a clean interface
A function's interface summarizes how to use it: what the parameters are, what the function does, and what it returns. An interface is clean when the caller can do what they want without handling unnecessary details. (p. 55)
Consider circle. A circle is approximated by a many-sided polygon. The circumference is
C=2πr,
and the polygon must satisfy length * n = circumference. With r=50, C≈314.2, so 50 segments would each be about 6.28 long. (pp. 54–55)
The radius r belongs in the interface because it says which circle to draw. The number of segments n is a rendering detail. Making the caller supply it would clutter the interface. A fixed n = 50 is also poor: huge circles get long, jagged segments, and tiny circles waste time on tiny segments. The better design lets the function choose n:
n = int(circumference / 3) + 3
length = circumference / n
This keeps each segment near 3 pixels long. Adding 3 guarantees at least 3 sides. (pp. 55–56)
Refactoring
The arc function draws only part of a circle, so polygon cannot be reused as it is. Its second half, however, looks like polygon. Giving polygon an angle parameter would make its name misleading, so the text creates a more general function, polyline(t, n, length, angle). Then polygon and arc both call polyline, and circle becomes simply arc(t, r, 360). (p. 56)
Refactoring is rearranging a working program to improve its interfaces and make code easier to reuse. Here the shared loop was "factored out" into polyline. Often you cannot design every interface at the start, because you learn about the problem as you code. Refactoring can be a sign that you have learned something. (p. 57)
The development plan
The encapsulation and generalization plan builds programs in small working steps:
- Write a small working program with no functions.
- Encapsulate a coherent piece in a named function.
- Generalize it by adding parameters.
- Repeat until you have working functions. Copy working code instead of retyping it.
- Refactor to merge similar code into general functions.
This suits situations where you do not know in advance how to split the program into functions. You design as you go. (p. 57)
Docstrings
A docstring is a triple-quoted string at the start of a function that explains its interface. The polyline docstring says it draws n segments of a given length with a given angle (in degrees) between them, and that t is a turtle. A good docstring says briefly what the function does, not how. It also describes each parameter's effect and its type if that is not obvious. If a function is hard to document, its interface may need improving. (pp. 57–58)
Preconditions, postconditions, and debugging
An interface acts like a contract between the function and its caller. Preconditions must be true before the function runs, and the caller is responsible for them. For polyline, t must be a Turtle, n an integer, length a positive number, and angle a number in degrees. Postconditions must be true afterward. They include the intended effect, such as the segments drawn, and side effects, such as the turtle's new position. (p. 58)
This tells you where a bug lives:
- A precondition is violated and the function misbehaves: the bug is in the caller.
- Preconditions hold but a postcondition fails: the bug is in the function. (p. 58)
Check yourself
- What is the difference between a function and a method? Give an example of calling a method on a turtle named
bob.
- Why does the square-drawing loop still return the turtle to its starting position and heading, even though it makes a fourth turn?
- Explain the difference between encapsulation and generalization, using
square as your example.
- Why is
n left out of the interface of circle, while r is included?
- A call to
polyline(bob, 5, -20, 72) draws something odd. Which precondition was broken, and who is responsible for the bug?