NumPy16/18
Starting sandbox…
Beginner·4 graded tests

Views vs Copies

Views vs Copies

Core Principle: Slicing creates a strided view sharing memory with the parent array, whereas boolean masking and integer indexing create independent memory copies.

The demonstration

[1]python
out1ms
array([  0,   1, 999,   3,   4,   5,   6,   7,   8,   9])

The table

Operation Result
a[2:5], a[::2], a[:, 0] view
a.T, a.reshape(...) (when compatible), a.ravel() view (usually)
a0, 2, 4 copy
a[a > 5] copy
a.flatten() copy (always)
a.astype(...) copy (always)
a.copy() copy
np.asarray(a) same object if already an array
a + 0, any arithmetic new array
np.concatenate, np.stack new array

Note ravel() vs flatten(): ravel returns a view when it can, flatten always copies. If you want a guaranteed-independent 1-D version, use flatten. If you want speed and will not write to it, use ravel.

reshape returns a view when the requested shape is compatible with the existing strides. After a transpose it usually is not, so a.T.reshape(-1) silently copies. See Memory Layout and Strides.

How to check

b.base is a           # True → b is a view of a
b.base                # None → b owns its data
b.flags.owndata       # False → it's a view
np.shares_memory(a, b)          # definitive, can be slow
np.may_share_memory(a, b)       # fast, conservative

b.base is a is the everyday tool. np.shares_memory is the one to reach for when debugging something genuinely confusing.

Why views exist

They are the reason NumPy scales.

[2]python
out1ms
array([[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.]], shape=(1000, 100000))

If slicing copied, every intermediate step in a pipeline over a large matrix would double your memory. Views make chained slicing free.

Where it hurts

1. The function that mutates its argument.

def normalise(matrix):
    matrix -= matrix.mean(axis=1, keepdims=True)   # IN-PLACE: mutates the caller's array!
    return matrix

expr_norm = normalise(expr)     # expr is now also normalised

The -= operates on the array the caller passed. Either document that clearly, or copy first:

[ ]python
out1ms

This code completed without producing output.

x = x - y rebinds to a new array; x -= y mutates in place. In NumPy those are genuinely different operations, unlike for immutable Python types.

2. Keeping a small slice of a huge array alive.

big = np.zeros((100_000, 1000))
small = big[0:10]
del big
# the full 800 MB is STILL allocated: `small` holds a reference through .base
small = big[0:10].copy()        # ← the fix

This is a real memory leak pattern when you slice a large file-loaded array and keep only the slice.

3. Chained assignment that goes nowhere.

[ ]python
out1ms

This code completed without producing output.

No error, no warning, no effect. This is the exact same failure mode as pandas chained assignment: see Copy-on-Write, which is why pandas 3.0 made it impossible.

Assignment is different from selection

[ ]python
out1ms

This code completed without producing output.

So masked reads copy but masked writes do modify the original. This asymmetry is correct but surprising the first time.

Defensive practice

  • Copy at API boundaries. A function that takes an array from a caller and mutates it should say so in its name (normalise_inplace) or copy.
  • Copy after slicing something large you want to keep. roi = big[a:b].copy().
  • Use np.shares_memory when debugging "why did this change".
  • Prefer explicit out= over relying on in-place operators when the intent matters. See ufuncs.

Bioinformatics example

# subsetting to a gene panel from a whole-transcriptome matrix
panel_idx = np.flatnonzero(np.isin(gene_names, PANEL))
panel_expr = expr[panel_idx]              # fancy indexing → copy, safe
del expr                                   # the big matrix is genuinely freed

# a chromosome window from a whole-genome coverage array
window = coverage[start:end]               # view: do NOT modify unless you mean to
window_own = coverage[start:end].copy()    # safe to modify

Common mistakes

  • Mutating a view and corrupting the source.
  • Expecting a mask-selected copy to write back. a[mask][0] = x does nothing.
  • A hidden memory leak from a small view of a big array.
  • ravel() when you needed flatten().
  • Assuming reshape never copies. After a transpose it usually does.
  • Confusing x = x + 1 with x += 1 on a view. The first is safe; the second writes through.

See also

Indexing and Slicing · Boolean Masking · Fancy Indexing · Memory Layout and Strides · Copy-on-Write · ndarray

Exercise

Write aliasing(values, threshold), which demonstrates which ways of selecting from an array give a view and which give a copy. Return ["empty"] for an empty array.

Take three selections: the strided slice values[1::2], the fancy selection values[[0, -1]], and the boolean selection values[values > threshold]. Return six lines: slice_shares=<bool>, slice_base_is_source=<bool>, fancy_shares=<bool> and mask_shares=<bool>, testing each with np.shares_memory and, for the slice only, also comparing .base against the original. Then assign 0 through the slice and 99 through the fancy selection and report after=<list>, the original array afterwards, and picked=<list>, the fancy selection afterwards. A single-element array is the case that separates the two slice tests: the strided slice is empty, so it shares no memory with anything, yet its .base is still the array it came from.

exerciseEvery submission runs independently in a fresh namespace.

Test cases · 4

#viainputexpected stdout
1stdin3 1 2 3 4 5 6 slice_shares=True slice_base_is_source=True fancy_shares=False mask_shares=False after=[1, 0, 3, 0, 5, 0] picked=[99, 99]
2stdin10 1 2 3 slice_shares=True slice_base_is_source=True fancy_shares=False mask_shares=False after=[1, 0, 3] picked=[99, 99]
3stdin0 7 slice_shares=False slice_base_is_source=True fancy_shares=False mask_shares=False after=[7] picked=[99, 99]
4stdin5 empty

Hints · 2

01Hint

np.shares_memory returns False for an empty selection because there is no overlapping byte to find.

02Hint

Writing through a view changes the source; writing through a fancy or boolean selection changes only the copy.