pythonforbio.
[WASM idle]
NumPy16/18

Views vs Copies

Starting sandbox…
Beginnerlesson2 tests

Views vs Copies

In one line: basic slicing gives you a window onto the same memory; boolean and fancy indexing give you new memory — and the difference decides whether your writes are visible elsewhere.

The demonstration

a = np.arange(10)

view = a[2:5]          # basic slicing → VIEW
view[0] = 999
a                       # array([0, 1, 999, 3, ...])  ← changed

copy = a[[2, 3, 4]]     # fancy indexing → COPY
copy[0] = -1
a                       # unchanged

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.

big = np.zeros((100_000, 1000))    # 800 MB
window = big[50_000:50_100]         # instant, 0 bytes
big.T                                # instant, 0 bytes

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:

def normalise(matrix):
    matrix = matrix - matrix.mean(axis=1, keepdims=True)    # new array
    return matrix

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.

a[a > 5][0] = 0       # a[a > 5] produces a COPY; the write hits a temporary and is lost
a[np.flatnonzero(a > 5)[0]] = 0    # correct

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

a[a > 5]        # __getitem__ → gather into new memory (copy)
a[a > 5] = 0    # __setitem__ → scatter into existing memory (in-place)

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

Test cases · 2

#viainputexpected stdout
1stdin1 2 3 4view_is_base=True original0=99 copy0=99
2stdin7 8 9view_is_base=True original0=99 copy0=99

Hints · 2

01Hint

Basic slicing returns a view that shares memory with the original.

02Hint

arr.base is not None when arr is a view of another array.

starter

No output yet — run the code to populate this drawer.

Vectorizationndarray