A list and an array, sliced and written to the same way. Watch the original in each case.
rows = [[1, 2], [3, 4]] part = rows[0] # a reference to the inner list part[0] = 99 rows # -> [[99, 2], [3, 4]] inner lists are shared import numpy as np grid = np.array([[1, 2], [3, 4]]) column = grid[:, 0] # a VIEW, not a copy column[:] = 0 grid # -> [[0, 2], [0, 4]] the array changed # And the version that does not: result = grid.copy() result[:, 0] = 0 # only result changes grid[:, 0].base is grid # -> True, the tell-tale of a view
- Basic slicing returns a view, and that is the whole design
grid[:, 0] hands back an object that shares the original’s memory and just reads it with a different stride — no data is copied, which is why slicing a million-row array is instant. The cost is that a view is a window onto the original, so writing through the window writes through to the array. A list slice copies, which is safe and would be unaffordable at numpy’s sizes.
- Fancy indexing copies; basic slicing does not
grid[:, [0]] and grid[grid > 2] return copies, because the result cannot be described as a stride over the original. grid[:, 0] and grid[1:3] return views. The distinction is not arbitrary and it is not guessable from the syntax alone — `.base` tells you which you have, and it is None for a copy.
- Assigning into a slice writes through the view
result[:, 0] = 0 does not rebind anything — it asks the view to write zeros into the memory it looks at. That is why the copy has to happen first, on the whole array, rather than on the slice: copying the slice and then writing into the copy changes something nobody is looking at.
Two lines. The order matters: something has to be copied before anything is written, and it is not the column.
grid.copy() gives an independent array. Then assigning into that copy’s column cannot reach the original. Copying the *slice* instead leaves you writing into something nobody will ever read.
result[:, index] = 0 — the colon is every row, the index is the column, and numpy broadcasts the single 0 across all of them. No loop, and it keeps the dtype, which building a fresh array from lists would not.
import numpy as np def blank_column(grid, index): result = grid.____() result[____, index] = 0 return ____
import numpy as np
def blank_column(grid, index):
# The copy has to be of the whole array, before anything is written.
# Copying the slice instead would leave the original untouched and the
# zeros somewhere nobody is looking.
result = grid.copy()
# [:, index] is every row of one column, and the single 0 broadcasts
# across all of them.
result[:, index] = 0
return result
Copy then write is correct; write then copy returns a correct-looking array and has already damaged the input. There is no version of this function where the copy is optional, and the reason it gets left out is that the return value is right either way — so only a test that looks at the *input* afterwards can tell the two apart.
result[:, index] = 0 assigns one number to n positions, because numpy stretches the scalar to the shape of the target. The same line works with an array on the right as long as its shape is compatible — which is the rule worth learning next, and the reason numpy code has so few loops in it.
`array.base` is the array a view is looking at, and None for something that owns its own data — so grid[:, 0].base is grid and grid.copy().base is None. When you are unsure whether an operation copied, that is the check, and it is worth running in a REPL rather than reasoning about from memory.
Do not copy defensively everywhere. The entire value of numpy is that a slice is free, and a function that copies a gigabyte array to change one column has thrown that away — which is why numpy’s own in-place operations exist and why many libraries take an `inplace` or `copy` argument instead of choosing for you. The rule is to be explicit: a function that modifies its argument should say so in its name, and one that does not should copy once, at the top. What you must never do is let it depend on whether the caller happened to pass a view.
- Write the in-place version, name it so the caller cannot miss what it does, and have it return None the way list.sort does.
- Check `.base` on grid[:, 0], grid[:, [0]] and grid[grid > 2], and work out from the results which of the three copied.
- Zero several columns at once by passing a list of indices, and notice that the assignment target is now fancy indexing while the assignment still works.