• No se han encontrado resultados

In the last chapter, we saw how classes could be used to structure data by combining several instance variables together into a single object. Lists and classes used together are powerful tools for structuring the data in our programs.

Remember theDieViewclass from last chapter? In order to display the six possible values of a die, eachDieViewobject keeps track of seven circles representing the position of pips on the face of a die. In the previous version, we saved these circles using instance variables,pip1,pip2,pip3, etc.

Let’s consider how the code looks using a collection of circle objects stored as a list. The basic idea is to replace our seven instance variables with a single list calledpips. Our first problem is to to create a suitable list. This will be done in the constructor for theDieViewclass.

In our previous version, the pips were created with this sequence of statements inside of init :

self.pip1 = self.__makePip(cx-offset, cy-offset) self.pip2 = self.__makePip(cx-offset, cy)

self.pip3 = self.__makePip(cx-offset, cy+offset) self.pip4 = self.__makePip(cx, cy)

self.pip5 = self.__makePip(cx+offset, cy-offset) self.pip6 = self.__makePip(cx+offset, cy)

self.pip7 = self.__makePip(cx+offset, cy+offset)

Recall that makePipis a local method of theDieViewclass that creates a circle centered at the postion given by its parameters.

We want to replace these lines with code to create a list of pips. One approach would be to start with an empty list of pips and build up the final list one pip at a time.

pips = [] pips.append(self.__makePip(cx-offset, cy-offset)) pips.append(self.__makePip(cx-offset, cy)) pips.append(self.__makePip(cx-offset, cy+offset)) pips.append(self.__makePip(cx, cy)) pips.append(self.__makePip(cx+offset, cy-offset)) pips.append(self.__makePip(cx+offset, cy)) pips.append(self.__makePip(cx+offset, cy+offset)) self.pips = pips

An even more straightforward approach is to create the list directly, enclosing the calls to makePip

inside list construction brackets, like this:

self.pips = [ self.__makePip(cx-offset, cy-offset)), self.__makePip(cx-offset, cy)), self.__makePip(cx-offset, cy+offset)), self.__makePip(cx, cy)), self.__makePip(cx+offset, cy-offset)), self.__makePip(cx+offset, cy)), self.__makePip(cx+offset, cy+offset)) ]

Notice how I have formatted this statement. Rather than making one giant line, I put one list element on each line. Python is smart enough to know that the end of the statement has not been reached until it finds the matching square bracket. Listing complex objects one per line like this makes it much easier to see what is happening. Just make sure to include the commas at the end of intermediate lines to separate the items of the list.

The advantage of a pip list is that it is much easier to perform actions on the entire set. For example, we can blank out the die by setting all of the pips to have the same color as the background.

for pip in self.pips:

pip.setFill(self.background)

See how these two lines of code loop through the entire collection of pips to change their color? This required seven lines of code in the previous version using separate instance variables.

Similarly, we can turn a set of pips back on by indexing the appropriate spot in the pips list. In the original program, pips 1, 4, and 7 were turned on for the value 3.

self.pip1.setFill(self.foreground) self.pip4.setFill(self.foreground) self.pip7.setFill(self.foreground)

In the new version, this corresponds to pips in positions 0, 3 and 6, since the pips list is indexed starting at 0. A parallel approach could accomplish this task with these three lines of code:

self.pips[0].setFill(self.foreground) self.pips[3].setFill(self.foreground) self.pips[6].setFill(self.foreground)

Doing it this way makes explicit the correspondence between the individual instance variables used in the first version and the list elements used in the second version. By subscripting the list, we can get at the individual pip objects, just as if they were separate variables. However, this code does not really take advantage of the new representation.

for i in [0,3,6]:

self.pips[i].setFill(self.foreground)

Using an index variable in a loop, we can turn all three pips on using the same line of code.

The second approach considerably shortens the code needed in thesetValuemethod of theDieView

class. Here is the updated algorithm:

Loop through pips and turn all off

Determine the list of pip indexes to turn on

Loop through the list of indexes and turn on those pips.

We could implement this algorthim using a multi-way selection followed by a loop.

for pip in self.pips:

self.pip.setFill(self.background) if value == 1: on = [3] elif value == 2: on = [0,6] elif value == 3: on = [0,3,6] elif value == 4: on = [0,2,4,6] elif value == 5: on = [0,2,3,4,6] else: on = [0,1,2,4,5,6] for i in on: self.pips[i].setFill(self.foreground)

The version without lists required 36 lines of code to accomplish the same task. But we can do even better than this.

Notice that this code still uses theif-elifstructure to determine which pips should be turned on. The correct list of indexes is determined byvalue; we can make this decision table-driven instead. The idea is to use a list where each item in the list is itself a list of pip indexes. For example, the item in position 3 should be the list[0,3,6], since these are the pips that must be turned on to show a value of 3.

Here is how a table-driven approach can be coded:

onTable = [ [], [3], [2,4], [2,3,4],

[0,2,4,6], [0,2,3,4,6], [0,1,2,4,5,6] ] for pip in self.pips:

self.pip.setFill(self.background) on = onTable[value]

for i in on:

self.pips[i].setFill(self.foreground)

I have called the table of pip indexesonTable. Notice that I padded the table by placing an empty list in the first position. Ifvalueis 0, theDieViewwill be blank. Now we have reduced our 36 lines of code to seven. In addition, this version is much easier to modify; if you want to change which pips are displayed for various values, you simply modify the entries inonTable.

There is one last issue to address. The onTablewill remain unchanged throughout the life of any particularDieView. Rather than (re)creating this table each time a new value is displayed, it would be better to create the table in the constructor and save it in an instance variable. Putting the definition of

class DieView:

""" DieView is a widget that displays a graphical representation of a standard six-sided die."""

def __init__(self, win, center, size): """Create a view of a die, e.g.:

d1 = GDie(myWin, Point(40,50), 20)

creates a die centered at (40,50) having sides of length 20."""

# first define some standard values self.win = win

self.background = "white" # color of die face self.foreground = "black" # color of the pips

self.psize = 0.1 * size # radius of each pip

hsize = size / 2.0 # half of size

offset = 0.6 * hsize # distance from center to outer pips

# create a square for the face

cx, cy = center.getX(), center.getY() p1 = Point(cx-hsize, cy-hsize) p2 = Point(cx+hsize, cy+hsize) rect = Rectangle(p1,p2) rect.draw(win) rect.setFill(self.background)

# Create 7 circles for standard pip locations

self.pips = [ self.__makePip(cx-offset, cy-offset), self.__makePip(cx-offset, cy), self.__makePip(cx-offset, cy+offset), self.__makePip(cx, cy), self.__makePip(cx+offset, cy-offset), self.__makePip(cx+offset, cy), self.__makePip(cx+offset, cy+offset) ] # Create a table for which pips are on for each value self.onTable = [ [], [3], [2,4], [2,3,4],

[0,2,4,6], [0,2,3,4,6], [0,1,2,4,5,6] ] self.setValue(1)

def __makePip(self, x, y):

"""Internal helper method to draw a pip at (x,y)""" pip = Circle(Point(x,y), self.psize)

pip.setFill(self.background) pip.setOutline(self.background) pip.draw(self.win)

return pip

def setValue(self, value):

""" Set this die to display value.""" # Turn all the pips off

for pip in self.pips:

# Turn the appropriate pips back on for i in self.onTable[value]:

self.pips[i].setFill(self.foreground)