On this page

References and Grids

In the last module a list was something you built and changed. This module is about a question that had no reason to come up until lists could change: when two names refer to a list, is that two lists or one? The answer explains a family of bugs that look like magic until you can draw them, and it is what makes a grid, a list of lists, work at all.

Before this, you can: change an item of a list; build a list with append in a loop; write nested loops.

By the end you will be able to say how many lists a program has made, make a real copy when you need one, tell a function that changes your list from one that only looks at it, read and write the cells of a grid, and build a grid whose rows are separate lists.

Before you read on, decide what this prints:

a = [1, 2, 3]
b = a
b[0] = 9
print(a[0])
9

The change was made through b. No line gives anything a new value through a, and yet a[0] has changed. If you know Java you have been told why: a variable holds a reference to an object. This module is about predicting with that fact, which is harder than knowing it.

Two names for one list

A name does not contain a list. A name refers to a list, the way a label is tied to a parcel. b = a does not make a second parcel. It ties a second label to the same one.

a ──┐
    ├──▶ [1, 2, 3]
b ──┘

So there is one list here, with two names. A change made through either name is a change to that one list, and the other name sees it, because there is nothing else to see.

Why did this never matter with numbers?

a = 5
b = a
b = 9
print(a)
5

Look at what the third line does. b = 9 does not change the number 5; it moves the label b to a different value. a is still tied to 5. The same is true of lists: giving a name a new value with = only moves that label.

a = [1, 2, 3]
b = a
b = [7]
print(len(a))
3

The difference is that a list can also be changed in place, by b[0] = 9, b.append(4), b.remove(2) and the rest, and a number cannot. Changing in place changes the parcel. Assigning with = moves a label. Every puzzle in this module comes down to asking which of the two a line does.

A copy is a second list

When you want a second list, ask for one. a.copy() makes a new list holding the same items, and so does the slice a[:]:

a = [1, 2, 3]
b = a.copy()
b[0] = 9
print(a[0], b[0])
1 9
a ──▶ [1, 2, 3]

b ──▶ [9, 2, 3]

Now there are two lists, and a change to one is not a change to the other.

Stepping through a program in the lab shows this picture directly: every list gets a number, and each name shows the number of the list it refers to. Two names with the same number are one list.

Functions and your lists

A parameter is a name. Calling a function ties that name to whatever was passed in, exactly as = would. So a function that is handed your list has a second name for your list, and if it changes the list in place, you see the change:

def add_one(items):
    items.append(1)

mine = [5]
add_one(mine)
print(mine)
[5, 1]

What a function cannot do is make your name refer to something else. Assigning to the parameter only moves the function's own label:

def reset(items):
    items = []

mine = [5]
reset(mine)
print(mine)
[5]

This is the rule from Functions, seen again: giving a parameter a new value changes nothing outside. It is only changes in place that reach the caller.

Both kinds of function are useful, and a reader should be able to tell which they have. A function that changes its argument usually returns nothing, like append. A function that leaves its argument alone builds a new list and returns it:

def with_one(items):
    longer = items.copy()
    longer.append(1)
    return longer

mine = [5]
more = with_one(mine)
print(mine, more)
[5] [5, 1]

Rows of rows

A list can hold lists. A list of rows, each row a list, is a grid, and it is how Python programs hold a board, a picture or a table.

grid = [
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12],
]
print(len(grid))
print(grid[1])
print(len(grid[1]))
3
[5, 6, 7, 8]
4

grid holds three items, and each is a list. len(grid) is the number of rows, grid[1] is the whole second row, and len(grid[1]) is the number of cells in that row. There is no separate grid type and no declared size. The rows could even have different lengths, though in this course they will not.

Two loops, one inside the other, visit every cell:

from scimigo import canvas, rectangle

canvas(600, 400)
picture = [
    [1, 0, 1, 0],
    [0, 1, 0, 1],
    [1, 0, 1, 0],
]
for row in range(len(picture)):
    for col in range(len(picture[row])):
        if picture[row][col] == 1:
            rectangle(100 + col * 50, 80 + row * 50, 48, 48, color="navy")

Row, then column

grid[row][col] is read from left to right. grid[row] picks out a row, which is a list, and [col] picks a cell out of that list. The row comes first.

grid = [
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12],
]
print(grid[1][2], grid[2][0])
grid[0][3] = 0
print(grid[0])
7 9
[1, 2, 3, 0]

On the canvas it is the other way round: x, which goes across like a column, is written first. Getting grid[col][row] when you meant grid[row][col] is the most common mistake with grids, so say "row, then column" to yourself when you write one.

A cell's position is two numbers that belong together. Round brackets make them into one value, a pair, and a pair can be taken apart into two names in one line:

cell = (2, 0)
row, col = cell
print(row, col)
2 0

A pair is like a list that cannot be changed: cell[0] works, and cell[0] = 5 is an error. That makes it a good fit for a position, which is a value, not a container.

Three rows, or one row three times?

* repeats a list: [0] * 4 is a list of four zeros. That is the quick way to make a row. It is tempting to use it again to make a grid:

grid = [[0] * 3] * 3
grid[0][0] = 5
print(grid[0])
print(grid[1])
print(grid[2])
[5, 0, 0]
[5, 0, 0]
[5, 0, 0]

One cell was set and a whole column changed. Nothing is wrong with Python. [row] * 3 makes a list holding the same row three times: three names for one list, exactly as in the first section.

grid ──▶ [ ●, ●, ● ]
           │  │  │
           └──┴──┴──▶ [5, 0, 0]

To get three separate rows, make three rows. Build each one inside a loop, so that the line that makes a row runs once for each row:

grid = []
for r in range(3):
    grid.append([0] * 3)
grid[0][0] = 5
print(grid[0])
print(grid[1])
[5, 0, 0]
[0, 0, 0]

The same trap waits when you copy a grid. grid.copy() makes a new outer list, but its items are the same rows as before, so changing a cell of the copy changes the original too. To copy a grid, copy every row:

grid = [[1, 2], [3, 4]]
shallow = grid.copy()
deep = []
for row in grid:
    deep.append(row.copy())
grid[0][0] = 99
print(shallow[0][0], deep[0][0])
99 1

In the lab

You will predict what an aliased change does, count the lists a program has made, decide whether a function changed your list, read cells of a grid by row and column, draw a picture stored as a grid, repair a seating chart whose rows are all one list, and write functions that count along a row and a column and that return a mirrored copy of a grid.

Your turn

Now try it yourself

Predict what a program will do, run it, and step through it line by line. The checks tell you when your own code works.

Loading the lab…

In this lab

  1. Two names

    Three short programs. Each has two names, a and b. For each, ask yourself whether a line changes a value in place or only moves a name, and decide what is printed.

  2. A real copy

    copy makes a second list with the same items, and so does a slice of everything. Picture the names and the lists, with an arrow from each name, and decide what each program prints.

  3. Keep the original

    This program is meant to remember the scores as they were, in original, and then change scores: the first score becomes 0 and a 99 is added. Run it. original has changed as well.

    Fix the line that makes original, so that it keeps the scores as they were while scores changes. Leave the other lines as they are. The checks try other lists.

  4. Who changed my list?

    A parameter is a second name for whatever was passed in. Three functions are each given the list mine. Decide what the caller's list holds afterwards.

  5. Row, then column

    A grid is a list of rows. grid[row] is one row, and grid[row][col] is one cell of it: the row first, then the column. Decide what each line prints.

  6. A pair, taken apart

    Two values in round brackets are a pair. A pair can be taken apart into two names in one line. Decide what each program does.

  7. The same thing, several times

    * repeats a list. Decide what each line prints.

  8. Pixel grid

    picture is a grid of 1s and 0s: a 1 is a filled square. The program draws the squares of the first row only. Change it so that it draws every row.

    The square for picture[row][col] is 48 by 48, coloured "navy", at x = 100 + 50 × col and y = 80 + 50 × row.

    The checks try pictures of other sizes.

  9. Seating chart

    A cinema has rows rows of cols seats. seats is a grid of 0s, and one seat is then marked as taken: row 1, column 2. Run the program. A whole column is marked.

    Say to yourself how many row lists the second-to-last line of the setup really makes. Then replace the line that builds seats with the few lines it takes to build the rows one at a time, so that every row is a list of its own and only the one seat is marked. Leave the rest of the program as it is.

  10. Count a row, count a column

    In a seating grid a 1 is a taken seat and a 0 a free one. Write two functions that return how many seats are taken:

    • taken_in_row(grid, row) counts along one row;
    • taken_in_column(grid, col) counts down one column.

    Neither prints anything or changes the grid. The two lines at the bottom then print 2 and 3.

  11. Mirror

    Write mirrored(grid), which returns a new grid with every row reversed, so that the picture is flipped left to right. The first cell of each row becomes the last.

    The grid you are given must be exactly as it was afterwards, and the new grid must not share any row with it. The function prints nothing.

  12. Smooth the heat map

    A grid of temperatures is noisy. Write smoothed(grid), which returns a new grid of the same size:

    • a cell on the edge (first or last row, first or last column) keeps its value;
    • every other cell becomes the average of its four neighbours, above, below, left and right, taken from the original grid.

    The original grid must be unchanged. The program then prints the middle row of the smoothed grid.

    The trap: if you change the grid you are reading from, the cells you have already smoothed feed into the ones after them. That is why the result goes in a new grid.

  13. Switch a cell

    Write two functions for a grid of lights, where 0 is off and 1 is on.

    • empty_grid(rows, cols) returns a grid of rows rows of cols zeros, in which every row is a list of its own.
    • toggle(grid, cell) takes a position as a pair, (row, col), and switches that one cell: 0 becomes 1, and 1 becomes 0. It changes the grid it is given and returns nothing.

    The lines below your functions then build a grid, switch four times, and print its two rows: [1, 0, 0] and [0, 0, 1].

  14. The undo that does nothing

    This program saves a grid, changes one cell, keeps the changed grid under the name changed, and then restores the saved one. Its last line should print 99 and then the cell's first value, which is 1 for the grid given. It prints 99 twice: nothing was restored.

    Before you change anything, say to yourself how many grids, and how many rows, the program has when it reaches line 3. Then fix the line that saves, so that at the end changed is the grid with the 99 in it and grid is the grid as it started. A warning: a copy of the outer list on its own is not enough here, and the lesson's last section says why.

  15. Explain it with arrows

    Do not run this program yet. Take a sheet of paper. For each line, draw the names and the lists as they are after that line: a name on the left, a list on the right, and an arrow from each name to the list it refers to.

    Then, at the bottom of the program, fill in the five comment lines with what you expect the last line to print for each name. Write the lists out in full.

    Only then run it, and step through it. Each list has a number in the lab, and each name shows the number of its list. Compare with your drawing line by line, and with what you wrote.

    There are no checks. Go through the list below when you are done.