Previous Up Next

Chapter 4  Case study: interface design

4.1  TurtleWorld

To accompany this book, I have written a suite of modules called Swampy. One of these modules is TurtleWorld, which provides a set of functions for drawing lines by steering turtles around the screen.

You can download Swampy from allendowney.com/swampy. Move into the directory that contains TurtleWorld.py, start the Python interpreter, and type:

>>> from TurtleWorld import *

This is a variation of the import statement we saw before. Instead of creating a module object, it imports the functions in the module directly, so we can access them without using dot notation. For example, to create TurtleWorld, type:

>>> TurtleWorld()

A window should appear on the screen and the interpreter should display something like:

<TurtleWorld.TurtleWorld instance at 0xb7f0c2ec>

The angle-brackets indicate that the return value from TurtleWorld is an instance of a TurtleWorld as defined in module TurtleWorld. In this context, an instance is a member of a set; this TurtleWorld is one of the set of possible TurtleWorlds.

To create a turtle, type:

>>> bob = Turtle()

In this case we assign the return value from Turtle to a variable named bob so we can refer to it later (we don't really have a way to refer to the TurtleWorld).

The turtle-steering functions are fd and bk for forward and backward, and lt and rt for left and right turns.

To draw a right angle, type:

>>> fd(bob, 100)
>>> lt(bob)
>>> fd(bob, 100)

The first line (and third) tells bob to take 100 steps forward. The second line tells him to turn right. In the TurtleWorld window you should see the turtle move east and then south, leaving two line segments behind.

Before you go on, use bk and rt to put the turtle back where it started.

4.2  Simple repetition

If you haven't already, move into the directory that contains TurtleWorld.py. Create a file named polygon.py and type in the code from the previous section:

from TurtleWorld import *
TurtleWorld()
bob = Turtle()

fd(bob, 100)
lt(bob)
fd(bob, 100)

When you run the program, you should see bob draw a right angle, but when the program finishes, the window disappears. Add the line

wait_for_user()

at the end of the program and run it again. Now the window stays up until you close it.

Now modify the program to draw a square. Don't turn the page until you've got it working!

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)

We can do the same thing more concisely with a for statement. Add this example to polygon.py and run it again:

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 turn the page until you do.

Here is a for statement that draws a square:

for i in range(4):
    fd(bob, 100)
    lt(bob)

The syntax of a for statement is similar to a function definition. It has a header that ends with a colon and an indented body. The body can contain any number of any kind of statement.

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 left 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.

4.3  Exercises

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).

  1. Write a function called square that takes a parameter named t, which is a turtle. It should use the turtle to draw a square.

    Write a function call that passes bob as an argument to square, and then run the program again.

  2. Add another parameter, named length, to square. Modify the body so length of the sides is length, and then modify the function call to provide a second argument. Run the program again. Test your program with a range of values for length.
  3. The functions lt and rt make 90-degree turns by default, but you can provide a second argument to that specifies the number of degrees. For example, lt(bob, 45) turns bob 45 degrees to the left.

    Make a copy of square and change the name to polygon. Add another parameter named n and modify the body so it draws an n-sided regular polygon. Hint: The angles of an n-sided regular polygon are 360.0 / n degrees.

  4. Write a function called circle that takes a turtle, t, and radius, r, as parameters and that draws an approximate circle by invoking polygon with an appropriate length and number of sides. Test your function with a range of values of r.

    Hint: figure out the circumference of the circle and make sure that length * n = circumference.

    Another hint: if bob is too slow for you, you can speed him up by changing bob.delay, which is the time between moves, in seconds. bob.delay = 0.01 ought to get him moving.

  5. Make a more general version of circle called arc that takes an additional parameter angle, which determines what fraction of a circle to draw. angle is in units of degrees, so when angle=360, arc should draw a complete circle.

4.4  Encapsulation

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:

square(bob)

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!

4.5  Generalization

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)

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

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, length, n):
    angle = 360.0 / n
    for i in range(n):
        fd(t, length)
        lt(t, angle)

polygon(bob, 70, 7)

This draws a 7-sides 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, length=70, n=7)

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.

One way to get started is to copy and modify polygon. Here is a simple solution:

def circle(t, r):
    circumference = 2 * math.pi * r
    n = 50
    length = circumference / n
    polygon(t, length, n)

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 the value of n adaptively, depending on circumference:

def circle(t, r):
    circumference = 2 * math.pi * r
    n = int(circumference / 4)
    length = circumference / n
    polygon(t, length, n)

Now the number of segments is (approximately) circumference/4, so the length of each segment is (approximately) 4, which is small enough that the circles look good, but big enough to be efficient, and appropriate for any size circle.

4.7  Refactoring

When we wrote circle, we were 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.

An 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):
    arclength = 2 * math.pi * r * angle / 360
    n = int(arclength / 4)
    length = arclength / n
    step_angle = angle / n
    
    for i in range(n):
        fd(t, 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, length, n, 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, length, n):
    angle = 360.0 / n
    polyline(t, length, n, angle)

def arc(t, r, angle):
    arclength = 2 * math.pi * r * angle / 360
    n = int(arclength / 4)
    length = arclength / n
    polyline(t, length, n, angle/n)

Finally, we can rewrite circle to use arc:

def circle(t, r):
    arc(t, r, 360.0)

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 in this case study is what I call “EGR” for “encapsulation, generalization and refactoring.” The steps of EGR are:

  1. Start by writing a small program with no function definitions.
  2. Once you get the program working, encapsulate it in a function and give it a name.
  3. Generalize the function by adding appropriate parameters.
  4. Repeat steps 1–3 until you have a set of working functions. Copy and paste working code to avoid retyping (and re-debugging).
  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.

EGR 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.

4.9  docstring

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, length, n, angle):
    """Draw 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)

This docstring is a triple-quoted string, also known as a multi-line string because the triple quotes allow the string to span more than one line.

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 mean that the interface could be improved.

4.10  Glossary

instance:
A member of a set. The TurtleWorld in this chapter is a member of the set of TurtleWorlds.
loop:
A compound statement that can execute the body more than once.
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).
interface:
A description of how to use a function, including the name and descriptions of the arguments and return value.
development plan:
A process for writing programs.
docstring:
A string that appears in a function definition to document the function's interface.

4.11  Exercises

  1. Write appropriate docstrings for polygon and circle.
  2. Draw a stack diagram that shows the state of the program while executing circle(bob, 100). You can do the arithmetic by hand or add print statements to the code.
  3. Write an appropriately general set of functions that can draw flowers like this:
  4. Write an appropriately general set of functions that can draw shapes like this:

Previous Up Next