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')
They are 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. 
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.
Are individual names for the rectangles needed earlier? No, the program only need 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()
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!