NumPy·Lesson 6·12 min·0/3 exercises

Views, copies and shared memory

Slicing doesn’t copy — and that will edit your data behind your back exactly once.

.base.copy()np.shares_memory.flags.owndataview vs copy

Watch it happen

Play it through, or step back and forth yourself.

a
0
1
2
0
1
2
3
0
1
2
3
4
5
6
7
8
9
10
11
shape (3, 4)
the same object
b = a
0
1
2
0
1
2
3
0
1
2
3
4
5
6
7
8
9
10
11
shape (3, 4)

Start with the simplest case. b = a doesn't copy anything — it's a second name for the same array. Everyone expects that one.

The idea

Slicing a Python list gives you a new list. Slicing a NumPy array gives you a view: a different window onto the same bytes. Nothing is copied, and writing through the window edits the original.

window = a[1:3, 1:3]
window[:] = 99
a                      # a has changed too

This is deliberate, and it's the reason lesson 4 spent so long on strides: a view is just a new offset and a new set of strides pointing into memory somebody else owns. Slicing a two-gigabyte array costs nothing because nothing moves.

It's also a genuinely nasty class of bug, because the damage lands somewhere you weren't looking. You slice out a training set, normalise it, and discover much later that you normalised the original too.

Hand-drawn notes showing a view as a second window onto the same memory, where writing through it changes the original, against a copy which owns separate memory.

Which operations copy?

The rule is mechanical once you think in strides:

# views — describable with an offset and strides
a[1:3]            a[:, ::2]         a.T
a.reshape(2, 6)   a.ravel()         a[np.newaxis]

# copies — the elements are scattered, or the type changed
a[[0, 2]]         a[a > 5]          a.flatten()
a.astype(float)   a + 0             np.sort(a)

Basic slicing is a view. Fancy indexing and boolean masks are copies, because you can't describe "rows 0 and 2" with a single stride. Anything that produces new values — arithmetic, a dtype change — obviously needs new memory.

ravel is the interesting one: it returns a view when the array is already contiguous and quietly copies when it isn't. flatten always copies, so use it when you want the guarantee.

How to check

np.shares_memory(a, b)       # exact answer; can be slow on huge arrays
np.may_share_memory(a, b)    # fast, errs on the side of "maybe"
b.base is None               # True means b owns its data
b.flags.owndata              # the same question, phrased positively

A wrinkle about .base. It points at the ultimate owner of the memory, not necessarily the array you sliced. Because a = np.arange(12).reshape(3, 4) is itself a view of the array arange produced, this happens:

w = a[1:3, 1:3]
w.base is a          # False — a doesn't own the memory either
w.base is a.base     # True  — both point at the original block
np.shares_memory(a, w)   # True — the question you actually meant

So use .base is None as a quick "do I own this?" check, and np.shares_memory when you need to know whether two specific arrays overlap.

Hand-drawn notes listing which NumPy operations return a view and which copy: slicing and reshape give views, fancy indexing and boolean masks copy.

Working safely

Two habits cover almost everything. Copy when you intend to modify .copy() is cheap to type and the bug it prevents is expensive. And when a function takes an array and modifies it, say so in the name, because the caller has no way to tell from the outside.

def normalise(x):            # returns something new — safe
    return (x - x.mean()) / x.std()

def normalise_inplace(x):    # edits the caller's array — say so
    x -= x.mean()
    x /= x.std()

Note x -= … versus x = x - …: the first writes into the existing memory (and therefore into every view of it), the second rebinds the name to a brand-new array and leaves the original alone. Same arithmetic, completely different consequences.

Practice

Write it yourself. The answer is there when you want it.

Putting the kettle on…

Starting up…

Write it yourself

not graded

Take window = a[1:3, 1:3] and print whether it shares memory with a. Set the whole window to 99, then print a — it changed underneath you. Now build a fresh (3, 4), take the same block with .copy(), set that to 99, and print again to show it didn't.

Write something and press Run — the output appears here.

Your turn

3 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.

Does a[0:2] share memory with a? Return the boolean.

your answer

Now the same question for a[[0, 2]] — fancy indexing rather than a slice. Return the boolean.

your answer

Set the first row of a copy of a to zero, then return a — which must come back unchanged. The starter gets it wrong.

your answer