Names and values: making a game board

Tuesday 27 August 2013This is over 11 years old. Be careful.

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 = [
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
]

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 = [0] * 8

Let’s try using that technique twice, once to make a row, and then again to fill out the board:

from pprint import pprint
# Construct the empty board
board = [ [0]*8 ] * 8
pprint(board)
[[0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0],
 [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
board[0][0] = 1
pprint(board)
[[1, 0, 0, 0, 0, 0, 0, 0],
 [1, 0, 0, 0, 0, 0, 0, 0],
 [1, 0, 0, 0, 0, 0, 0, 0],
 [1, 0, 0, 0, 0, 0, 0, 0],
 [1, 0, 0, 0, 0, 0, 0, 0],
 [1, 0, 0, 0, 0, 0, 0, 0],
 [1, 0, 0, 0, 0, 0, 0, 0],
 [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:

row is a list with eight references to the same zerorow0row = [0] * 8

When we make the board, the same behavior of replicating a list applies, so the board is actually eight references to the same list:

board is a list of eight references to the rowboard0board = [ [0]*8 ] * 8

When we assign to board[0][0], 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:

changing only one elementboard01board[0][0] = 1

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
board = [ [0] * 8 ] * 8

# Make a row, then make a board
row = [0] * 8
board = [ row ] * 8

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[0][0], board[0][1], ..., board[0][7]. 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 = [ [0] * 8 for i in range(8) ]

With this code, we use [0]*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:
board1 = [ [0] * 8 ] * 8

# List comprehension, does work right:
board2 = [ [0] * 8 for i in range(8) ]

In the first case, [0]*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, [0]*8 is the expression in the list comprehension, and so is evaluated anew eight times, producing eight distinct lists to serve as our rows.

If this is still confusing, Facts and myths about Python names and values has more details, or try the code on pythontutor.com.

Comments

[gravatar]
Maybe dictionaries should be more popular and marketed? To me it feels that dict is great fit for such simple gameboards.
[gravatar]
I'd use a numpy ndarray for 2D tables. It's both easier and faster, but it adds some dependancies.
[gravatar]
Nice article. I remember doing Tic-Tac-Toe as my first Python project years ago. Making the board data structure was actually easy using a single list (e.g., ['1','2','3','4','5','6','7','8','9']) but the problem more the brute-forced index scanning algorithm I felt was needed to work with it. But that's often the problem with using improper data structures and are beginner problems more than Python's fault.
[gravatar]
Russell Borogove 7:24 PM on 28 Aug 2013
I'm with Marin on this: the correct way to create a gameboard:
from collections import defaultdict
gameboard = defaultdict( lambda:0 )
gameboard[0,0] = 1
[gravatar]
@Russel: couldn't you at least have used defaultdict(int) ?

:-)
[gravatar]
Russell Borogove 8:12 PM on 28 Aug 2013
@Ned -- yeah, I was going back and forth between 0 and None for my example as I typed it and got a little non-optimal.

(Odd -- why does defaultdict(NoneType) not work?)
[gravatar]
Probably because you can't call NoneType and get an instance:

>>> import types
>>> types.NoneType()
Traceback (most recent call last):
File "", line 1, in
TypeError: cannot create 'NoneType' instances
[gravatar]
If you're unhappy with the amount of control graphviz gives you, but don't want to draw by hand, you may be interested in tikz. It's a LaTeX thing, but if you like it drop me a message and I can show you how to export it to svg.
[gravatar]
> Probably because you can't call NoneType and get an instance

You can use defaultdict(print) (in Py3) though. :-DD
[gravatar]
there's still still subtlety i have never seen explained:
u=[[0]]*8
v=[0]*8
or
u=[0]
v=u*8
w=[u]*8
u.append(1)
[gravatar]
Boris: try http://truepython.blogspot.com/2017/12/variables.html.

In short, you have as many lists as []'s (pairs of brackets) you evaluate. In the first case three, in the second case two of them.
[gravatar]
oh thanks! but should it be two and one pair of brackets in my examples?
also it seems more than a consequence of the rule I'm seeking than the rule itself
[gravatar]
Well, it depends on what exactly "your examples" are.

u=[[0]]*8
v=[0]*8

In the above, I see three []'s. So you construct three lists. First just contains 0, second contains first one eight times, and third contains 0 eight times.

Yes, it's the consequence of a general rule: evaluating literals constructs objects, variables just give them names. (As opposed to C++, for example, where declaring a variable makes an object.) As I said, read the linked post.
[gravatar]
I'm not sure I understand:
u=[0]
v=u*8
here I have two lists and doing
u.append(1)
only modify the first one
[gravatar]
Ah, yes. Literals make objects, but they aren't the only ones: arithmetical operators (and calls) can also make them (after all, literals can be seen as just class calls: [] is list()). The important point is, naming doesn't.

And another important point is, when arithmetical operations do make a new object, they make only one: their result. [u]*8 makes a new list (discarding the [u] operand, because nothing refers to it anymore), but only one, not eight of them.
[gravatar]
sure I can also find some ad hoc rules but I wish the fundamentals rules ("axioms") were given.
[gravatar]
The only fundamendal rule is that creating objects is dynamic operation, not static as in "old school" languages. So, asking whether something will create a new object is the same as asking if it will print the letter 'a' on screen. In general, it's undecidable, but of course you can have various heuristics ("rules of thumb") for deciding in many real-life cases.

One important rule of thumb is what I told you above: in all of Python's builtin namespaces, naming ("assignment") doesn't create any new objects. (Of course, you can invent your own namespace, where naming can do whatever you want.) It's the same as saying that in Python's builtin namespaces, naming doesn't print 'a' on the screen -- something that you can rely on, unless you're dealing with really strange codebases.

Add a comment:

Ignore this:
Leave this empty:
Name is required. Either email or web are required. Email won't be displayed and I won't spam you. Your web site won't be indexed by search engines.
Don't put anything here:
Leave this empty:
Comment text is Markdown.