Chapter 4 Case study: interface design
Code examples from this chapter are available from http://thinkpython.com/code/polygon.py.
To accompany this book, I have written a package called Swampy. You can download Swampy from http://thinkpython.com/swampy; follow the instructions there to install Swampy on your system.
If Swampy is installed as a package on your system, you can import TurtleWorld like this:
from swampy.TurtleWorld import *
If you downloaded the Swampy modules but did not install them as a package, you can either work in the directory that contains the Swampy files, or add that directory to Python’s search path. Then you can import TurtleWorld like this:
from TurtleWorld import *
The details of the installation process and setting Python’s search path depend on your system, so rather than include those details here, I will try to maintain current information for several systems at http://thinkpython.com/swampy
Create a file named mypolygon.py and type in the following code:
from swampy.TurtleWorld import * world = TurtleWorld() bob = Turtle() print bob wait_for_user()
The next lines create a TurtleWorld assigned to world and a Turtle assigned to bob. Printing bob yields something like:
<TurtleWorld.Turtle instance at 0xb7bfbf4c>
TurtleWorld provides several turtle-steering functions: fd and bk for forward and backward, and lt and rt for left and right turns. Also, each Turtle is holding a pen, which is either down or up; if the pen is down, the Turtle leaves a trail when it moves. The functions pu and pd stand for “pen up” and “pen down.”
To draw a right angle, add these lines to the program
(after creating bob and before calling
fd(bob, 100) lt(bob) fd(bob, 100)
The first line tells bob to take 100 steps forward. The second line tells him to turn left.
When you run this program, you should see bob move east and then north, leaving two line segments behind.
Now modify the program to draw a square. Don’t go on until you’ve got it working!
4.2 Simple repetition
Chances are you wrote something like this (leaving out the code that creates TurtleWorld and waits for the user):
fd(bob, 100) lt(bob) fd(bob, 100) lt(bob) fd(bob, 100) lt(bob) fd(bob, 100)
for i in range(4): print 'Hello!'
You should see something like this:
Hello! Hello! Hello! Hello!
This is the simplest use of the for statement; we will see more later. But that should be enough to let you rewrite your square-drawing program. Don’t go on until you do.
Here is a for statement that draws a square:
for i in range(4): fd(bob, 100) lt(bob)
A for statement is sometimes called a loop because the flow of execution runs through the body and then loops back to the top. In this case, it runs the body four times.
This version is actually a little different from the previous square-drawing code because it makes another turn after drawing the last side of the square. The extra turn takes a little more time, but it simplifies the code if we do the same thing every time through the loop. This version also has the effect of leaving the turtle back in the starting position, facing in the starting direction.
The following is a series of exercises using TurtleWorld. They are meant to be fun, but they have a point, too. While you are working on them, think about what the point is.
The following sections have solutions to the exercises, so don’t look until you have finished (or at least tried).
The first exercise asks you to put your square-drawing code into a function definition and then call the function, passing the turtle as a parameter. Here is a solution:
def square(t): for i in range(4): fd(t, 100) lt(t) square(bob)
The innermost statements, fd and lt are indented twice to show that they are inside the for loop, which is inside the function definition. The next line, square(bob), is flush with the left margin, so that is the end of both the for loop and the function definition.
Inside the function, t refers to the same turtle bob refers to, so lt(t) has the same effect as lt(bob). So why not call the parameter bob? The idea is that t can be any turtle, not just bob, so you could create a second turtle and pass it as an argument to square:
ray = Turtle() square(ray)
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!
The next step is to add a length parameter to square. Here is a solution:
def square(t, length): for i in range(4): fd(t, length) lt(t) square(bob, 100)
The next step is also a generalization. Instead of drawing squares, polygon draws regular polygons with any number of sides. Here is a solution:
def polygon(t, n, length): angle = 360.0 / n for i in range(n): fd(t, length) lt(t, angle) polygon(bob, 7, 70)
This draws a 7-sided polygon with side length 70. If you have more than a few numeric arguments, it is easy to forget what they are, or what order they should be in. It is legal, and sometimes helpful, to include the names of the parameters in the argument list:
polygon(bob, n=7, length=70)
This syntax makes the program more readable. It is also a reminder about how arguments and parameters work: when you call a function, the arguments are assigned to the parameters.
4.6 Interface design
The next step is to write circle, which takes a radius, r, as a parameter. Here is a simple solution that uses polygon to draw a 50-sided polygon:
def circle(t, r): circumference = 2 * math.pi * r n = 50 length = circumference / n polygon(t, n, length)
The first line computes the circumference of a circle with radius r using the formula 2 π r. Since we use math.pi, we have to import math. By convention, import statements are usually at the beginning of the script.
n is the number of line segments in our approximation of a circle, so length is the length of each segment. Thus, polygon draws a 50-sides polygon that approximates a circle with radius r.
One limitation of this solution is that n is a constant, which means that for very big circles, the line segments are too long, and for small circles, we waste time drawing very small segments. One solution would be to generalize the function by taking n as a parameter. This would give the user (whoever calls circle) more control, but the interface would be less clean.
The interface of a function is a summary of how it is used: what are the parameters? What does the function do? And what is the return value? An interface is “clean” if it is “as simple as possible, but not simpler. (Einstein)”
In this example, r belongs in the interface because it specifies the circle to be drawn. n is less appropriate because it pertains to the details of how the circle should be rendered.
Rather than clutter up the interface, it is better to choose an appropriate value of n depending on circumference:
def circle(t, r): circumference = 2 * math.pi * r n = int(circumference / 3) + 1 length = circumference / n polygon(t, n, length)
Now the number of segments is (approximately) circumference/3, so the length of each segment is (approximately) 3, which is small enough that the circles look good, but big enough to be efficient, and appropriate for any size circle.
When I wrote circle, I was able to re-use polygon because a many-sided polygon is a good approximation of a circle. But arc is not as cooperative; we can’t use polygon or circle to draw an arc.
One alternative is to start with a copy of polygon and transform it into arc. The result might look like this:
def arc(t, r, angle): arc_length = 2 * math.pi * r * angle / 360 n = int(arc_length / 3) + 1 step_length = arc_length / n step_angle = float(angle) / n for i in range(n): fd(t, step_length) lt(t, step_angle)
The second half of this function looks like polygon, but we can’t re-use polygon without changing the interface. We could generalize polygon to take an angle as a third argument, but then polygon would no longer be an appropriate name! Instead, let’s call the more general function polyline:
def polyline(t, n, length, angle): for i in range(n): fd(t, length) lt(t, angle)
Now we can rewrite polygon and arc to use polyline:
def polygon(t, n, length): angle = 360.0 / n polyline(t, n, length, angle) def arc(t, r, angle): arc_length = 2 * math.pi * r * angle / 360 n = int(arc_length / 3) + 1 step_length = arc_length / n step_angle = float(angle) / n polyline(t, n, step_length, step_angle)
Finally, we can rewrite circle to use arc:
def circle(t, r): arc(t, r, 360)
This process—rearranging a program to improve function interfaces and facilitate code re-use—is called refactoring. In this case, we noticed that there was similar code in arc and polygon, so we “factored it out” into polyline.
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 interfaces. Once you start coding, you understand the problem better. Sometimes refactoring is a sign that you have learned something.
4.8 A development plan
A development plan is a process for writing programs. The process we used in this case study is “encapsulation and generalization.” The steps of this process are:
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.
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(t, n, length, angle): """Draws n line segments with the given length and angle (in degrees) between them. t is a turtle. """ for i in range(n): fd(t, length) lt(t, angle)
It is terse, but it contains the essential information someone would need to use this function. It explains concisely what the function does (without getting into the details of how it does it). It explains what effect each parameter has on the behavior of the function and 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 are having a hard time explaining one of your functions, that might be a sign that the interface could be improved.
An interface is like a contract between a function and a caller. The caller agrees to provide certain parameters and the function agrees to do certain work.
For example, polyline requires four arguments: t has to be a Turtle; n is the number of line segments, so it 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 in the World).
Preconditions are the responsibility of the caller. If the caller violates a (properly documented!) precondition and the function doesn’t work correctly, the bug is in the caller, not the function.
Download the code in this chapter from http://thinkpython.com/code/polygon.py.
Write an appropriately general set of functions that can draw flowers as in Figure 4.1.
Write an appropriately general set of functions that can draw shapes as in Figure 4.2.
The letters of the alphabet can be constructed from a moderate number of basic elements, like vertical and horizontal lines and a few curves. Design a font that can be drawn with a minimal number of basic elements and then write functions that draw letters of the alphabet.
You should write one function for each letter, with names
Are you using one of our books in a class?We'd like to know about it. Please consider filling out this short survey.