3.2. Loops and Tuples

This section will discuss several improvements to the chooseButton1.py program from the last section that will turn it into example program chooseButton2.py.

First an introduction to tuples, which we will use for the first time in this section:

A tuple is similar to a list except that a literal tuple is enclosed in parentheses rather than square brackets, and a tuple is immutable. In particular you cannot change the length or substitute elements, unlike a list. Examples are

(1, 2, 3)
('yes', 'no')

Making a tuple is another way to make several items into a single object. You can refer to individual parts with indexing, like with lists, but a more common way is with multiple assignment. A silly simple example:

tup = (1, 2)
(x, y) = tup
print(x)  # prints 1
print(y)  # prints 2

Now back to improving the chooseButton1.py program, which has similar code repeating in several places. Imagine how much worse it would be if there were more colors to choose from and more parts to color!

First consider the most egregious example:

if isInside(pt, redButton):
    color = 'red'
elif isInside(pt, yellowButton):
    color = 'yellow'
elif isInside(pt, blueButton):
    color = 'blue'
else :
    color = 'white'

Not only is this exact if statement repeated several times, all the conditions within the if statement are very similar! Part of the reason I did not put this all in a function was the large number of separate variables. On further inspection, the particular variables redButton, yellowButton, blueButton, all play a similar role, and their names are not really important, it is their associations that are important: that redButton goes with ‘red’, .... When there is a sequence of things all treated similarly, it suggests a list and a loop. An issue here is that the changing data is paired, a rectangle with a color string. There are a number of ways to handle such associations. A very neat way in Python to package a pair (or more things together) is a tuple, turning several things into one object, as in (redButtton, ‘red’). Objects such are this tuple can be put in a larger list:

choicePairs = [(redButtton, 'red'), (yellowButton, 'yellow'),
               (blueButton, 'blue')]

Such tuples may be neatly handled in a for statement. You can imagine a function to encapsulate the color choice starting:

def getChoice(choicePairs, default, win):
    '''Given a list choicePairs of tuples, with each tuple in the form
    (rectangle, choice), return the choice that goes with the rectangle
    in win where the mouse gets clicked, or return default if the click
    is in none of the rectangles.'''

    point = win.getMouse()
    for (rectangle, choice) in choicePairs:
        #....

This is the first time we have had a for loop going through a list of tuples. Recall that we can do multiple assignments at once with tuples. This also works in a for loop heading. The for loop goes through one tuple in the list choicePairs at a time. The first time through the loop the tuple taken from the list is (redButtton, ‘red’). This for loop does a multiple assignment to (rectangle, choice) each time through the loop, so the first time rectangle refers to redButton and choice refers to 'red'. The next time through the loop, the second tuple from the list is used, (yellowButton, 'yellow') so this time inside the loop rectangle will refer to yellowButton and choice refers to 'yellow'.... This is a neat Python feature. [1]

There is still a problem. We could test each rectangle in the for-each loop, but the original if-elif ... statement in chooseButton1.py stops when the first condition is true. However for-each statements are designed to go all the way through the sequence. There is a simple way out of this in a function: A return statement always stops the execution of a function. When we have found the rectangle containing the point, the function can return the desired choice immediately!

def getChoice(choicePairs, default, win):
    '''Given a list of tuples (rectangle, choice), return the choice
    that goes with the rectangle in win where the mouse gets clicked,
    or return default if the click is in none of the rectangles.'''

    point = win.getMouse()
    for (rectangle, choice) in choicePairs:
        if isInside(point, rectangle):
            return choice
    return default

Note that the else part in chooseButton1.py corresponds to the statement after the loop above. If execution gets past the loop, then none of the conditions tested in the loop was true.

With appropriate parameters, the looping function is a complete replacement for the original if-elif statement! The replacement has further advantages.

  • There can be an arbitrarily long list of pairs, and the exact same code works.
  • This code is clearer and easier to read, since there is no need to read through a long sequence of similar if-elif clauses.
  • The names of the rectangles in the tuples in the list are never referred to. They are unnecessary here. Only a list needs to be specified. That could be useful earlier in the program ....

Are individual names for the rectangles needed earlier? No, the program only needs to end up with the pairs of the form (rectangle, color) in a list. The statements in the original program, below, have a similar form which will allow them to be rewritten:

redButton = makeColoredRect(Point(310, 350), 80, 30, 'red', win)
yellowButton = makeColoredRect(Point(310, 310), 80, 30, 'yellow', win)
blueButton = makeColoredRect(Point(310, 270), 80, 30, 'blue', win)

As stated earlier, we could use the statements above and then make a list of pairs with the statement

choicePairs = [(redButtton, 'red'), (yellowButton, 'yellow'),
               (blueButton, 'blue')]

Now I will look at an alternative that would be particularly useful if there were considerably more buttons and colors.

All the assignment statements with makeColorRect have the same format, but differing data for several parameters. I use that fact in the alternate code:

choicePairs = list()
buttonSetup = [(310, 350, 'red'), (310, 310, 'yellow'),
               (310, 270, 'blue')]
for (x, y, color) in buttonSetup:
   button = makeColoredRect(Point(x, y), 80, 30, color, win)
   choicePairs.append((button, color))

I extract the changing data from the creation of the rectangles into a list, buttonSetup. Since more than one data items are different for each of the original lines, the list contains a tuple of data from each of the original lines. Then I loop through this list and not only create the rectangles for each color, but also accumulates the (rectangle, color) pairs for the list choicePairs.

Note the double parentheses in the last line of the code. The outer ones are for the method call. The inner ones create a single tuple as the parameter.

Assuming I do not need the original individual names of the Rectangles, this code with the loop will completely substitute for the previous code with its separate lines with the separate named variables and the recurring formats.

This code has advantages similar to those listed above for the getChoice code.

Now look at what this new code means for the interactive part of the program. The interactive code directly reduces to

msg = Text(Point(win.getWidth()/2, 375),'Click to choose a house color.')
msg.draw(win)
color = getChoice(colorPairs, 'white', win)
house.setFill(color)

msg.setText('Click to choose a door color.')
color = getChoice(colorPairs, 'white', win)
door.setFill(color)

In the original version with the long if-elif statements, the interactive portion only included portions for the user to set the color of two shapes in the picture (or you would have been reading code forever). Looking now at the similarity of the code for the two parts, we can imagine another loop, that would easily allow for many more parts to be colored interactively.

There are still several differences to resolve. First the message msg is created the first time, and only the text is set the next time. That is easy to make consistent by splitting the first part into an initialization and a separate call to setText like in the second part:

msg = Text(Point(win.getWidth()/2, 375),'')
msg.draw(win)

msg.setText('Click to choose a house color.')

Then look to see the differences between the code for the two choices. The shape object to be colored and the name used to describe the shape change: two changes in each part. Again tuples can store the changes of the form (shape, description). This is another place appropriate for a loop driven by tuples. The (shape, description) tuples should be explicitly written into a list that can be called shapePairs. We could easily extend the list shapePairs to allow more graphics objects to be colored. In the code below, the roof is added.

The new interactive code can start with:

shapePairs = [(house, 'house'), (door, 'door'), (roof, 'roof')]
msg = Text(Point(win.getWidth()/2, 375),'')
msg.draw(win)
for (shape, description) in shapePairs:
    prompt = 'Click to choose a ' + description + ' color.'
    # ....

Can you finish the body of the loop? Look at the original version of the interactive code. When you are done thinking about it, go on to my solution. The entire code is in example program chooseButton2.py, and also below. The changes from chooseButton1.py are in three blocks, each labeled #NEW in the code. The new parts are the getChoice function and the two new sections of main with the loops:

'''Make a choice of colors via mouse clicks in Rectangles --
Demonstate loops using lists of tuples of data.'''

from graphics import *

def isBetween(x, end1, end2):
    '''Return True if x is between the ends or equal to either.
    The ends do not need to be in increasing order.'''
    
    return end1 <= x <= end2 or end2 <= x <= end1

def isInside(point, rect):
    '''Return True if the point is inside the Rectangle rect.'''
    
    pt1 = rect.getP1()
    pt2 = rect.getP2()
    return isBetween(point.getX(), pt1.getX(), pt2.getX()) and \
           isBetween(point.getY(), pt1.getY(), pt2.getY())

def makeColoredRect(corner, width, height, color, win):
    ''' Return a Rectangle drawn in win with the upper left corner
    and color specified.'''

    corner2 = corner.clone()  
    corner2.move(width, -height)
    rect = Rectangle(corner, corner2)
    rect.setFill(color)
    rect.draw(win)
    return rect

def getChoice(choicePairs, default, win):     #NEW
    '''Given a list choicePairs of tuples with each tuple in the form
    (rectangle, choice), return the choice that goes with the rectangle
    in win where the mouse gets clicked, or return default if the click
    is in none of the rectangles.'''

    point = win.getMouse()
    for (rectangle, choice) in choicePairs:
        if isInside(point, rectangle):
            return choice
    return default
        
        
def main():
    win = GraphWin('pick Colors', 400, 400)
    win.yUp() 

    #NEW
    choicePairs = list()
    buttonSetup = [(310, 350, 'red'), (310, 310, 'yellow'), (310, 270, 'blue')]
    for (x, y, color) in buttonSetup:
       button = makeColoredRect(Point(x, y), 80, 30, color, win)
       choicePairs.append((button, color))

    house = makeColoredRect(Point(60, 200), 180, 150, 'gray', win)
    door = makeColoredRect(Point(90, 150), 40, 100, 'white', win)
    roof = Polygon(Point(50, 200), Point(250, 200), Point(150, 300))
    roof.setFill('black')
    roof.draw(win)
    
    #NEW
    shapePairs = [(house, 'house'), (door, 'door'), (roof, 'roof')] 
    msg = Text(Point(win.getWidth()/2, 375),'')
    msg.draw(win)
    for (shape, description) in shapePairs:
        prompt = 'Click to choose a ' + description + ' color.'
        msg.setText(prompt)
        color = getChoice(choicePairs, 'white', win)
        shape.setFill(color)

    win.promptClose(msg)

main()

Run it.

With the limited number of choices in chooseButton1.py, the change in length to convert to chooseButton2.py is not significant, but the change in organization is significant if you try to extend the program, as in the exercise below. See if you agree!

3.2.1. Exercises

3.2.1.1. Choose Button Exercise

  1. Write a program chooseButton3.py, modifying chooseButton2.py. Look at the format of the list buttonSetup, and extend it so there is a larger choice of buttons and colors. Add at least one button and color.
  2. Further extend the program chooseButton3.py by adding some further graphical object shape to the picture, and extend the list shapePairs, so they can all be interactively colored.
  3. (Optional) If you would like to carry this further, also add a prompt to change the outline color of each shape, and then carry out the changes the user desires.
  4. (Optional Challenge) ** Look at the pattern within the list buttonSetup. It has a consistent x coordinate, and there is a regular pattern to the change in the y coordinate (a consistent decrease each time). The only data that is arbitrary each time is the sequence of colors. Write a further version chooseButton4.py with a function makeButtonSetup, that takes a list of color names as a parameter and uses a loop to create the list used as buttonSetup. End by returning this list. Use the function to initialize buttonSetup. If you like, make the function more general and include parameters for the x coordinate, the starting y coordinate and the regular y coordinate change.
[1]Particularly in other object-oriented languages where lists and tuples are way less easy to use, the preferred way to group associated objects, like rectangle and choice, is to make a custom object type containing them all. This is also possible and often useful in Python. In some relatively simple cases, like in the current example, use of tuples can be easier to follow, though the approach taken is a matter of taste. The topic of creating custom type of objects will not be taken up in this tutorial.