|Ned Batchelder : Blog | Code | Text | Site|
Names and values: making a game board
» Home : Blog : August 2013
In Facts and myths about Python names and values, I described how Python's names and values work. There I posed some other questions, one of which was:
Why do beginners find it hard to make a tic-tac-toe board in Python?
Writing a game often involves a grid-like board. Python doesn't have a native two-dimensional array, so you can use a list of lists. Making a checkerboard full of zeros can be done with a literal list:
board = [
But that's tedious, especially if you need a 100×100 board. Even if you don't, it seems like you could do it more compactly. Python lets you multiply lists by integers to replicate elements, so you can use this code to produce a list of eight zeros:
row =  * 8
Let's try using that technique twice, once to make a row, and then again to fill out the board:
# Construct the empty board
[[0, 0, 0, 0, 0, 0, 0, 0],
It works! Now let's change one of the cells:
# Put a 1 in the upper-left corner
[[1, 0, 0, 0, 0, 0, 0, 0],
Huh? Somehow setting one cell changed the first cell in every row!? What's going on?
Just as with assignment, list multiplication doesn't copy data (Fact: assignment never copies data.) When we replicate a list with multiplication, we get the same value referenced many times. When we made the single row, we made a list with eight references to the same zero:
When we make the board, the same behavior of replicating a list applies, so the board is actually eight references to the same list:
(BTW: if you know how to make graphviz draw diagrams with more manual control, I'd love to hear from you...)
When we assign to board, we are changing one element, but that element is visible through all eight rows, because all eight rows are actually references to the same list:
In Facts and Myths, I called this the Mutable Presto-Chango.
Perhaps this is clearer if you consider that these two ways to make the board are exactly the same:
# Make it all at once
Assigning the row to its own variable doesn't change the behavior, but when we see it assigned to a name, it's more obvious that you'll get eight references to the same row.
OK, but why didn't setting one element change all 64 zeros? The diagram shows that there's only one zero, referenced by all the elements of the board.
Two names can refer to the same value. In fact, in our case, eight names refer to the same value. The names are board, board, ..., board. Changing one of those names to refer to 1 won't make all of them refer to it, any more than if the eight names had been "a", "b", ..., "h". (Fact: Names are reassigned independently of other names.)
The compact way to make a board that works correctly is to use a list comprehension to make each row:
board = [  * 8 for i in range(8) ]
With this code, we use *8 to make the row, which is fine because assignment will correctly update a single element in that row. The list comprehension makes a list with eight of those, but instead of each one being a reference to the same row, the row is recomputed each time, so a new list is made for each row.
It's important to keep straight two different syntaxes that look very similar:
# List multiplication, doesn't work right:
In the first case, *8 is calculated once, placed into a one-element list, and then replicated 8 times, giving 8 references to the same row. In the second case, *8 is the expression in the list comprehension, and so is evaluated anew eight times, producing eight distinct lists to serve as our rows.
tagged: python» 8 reactions