Skillversitas

Views and Copies

Most NumPy operations do not allocate new memory - they return a view: a lightweight window that references the original array's buffer through an offset, shape, and strides. Only specific operations - fancy indexing, dtype conversion, arithmetic - allocate a copy. Knowing which is which explains both the performance wins (free sub-arrays) and the classic bugs (silent aliasing). The goal of this chapter is to make the distinction a reflex: know when your slice shares memory, and when to force a copy.

What a View Is

import numpy as np

a = np.arange(12)
b = a[2:8]                 # a view - no data copied
print(b.base is a)         # True - b's memory is owned by a
print(b.strides, b.shape)  # (8,) (6,) - same 8-byte stride as a

# a view is a window: shape + offset + strides, one shared buffer
c = a.reshape(3, 4)
d = c[1, 1:3]              # a view of a view - still one buffer
print(d.base is a)         # True - the base is the original owner

A view is a (buffer, offset, shape, strides) tuple. The buffer is shared with the owner; only the window parameters differ. That is why slicing is free even on huge arrays - no memcpy, just pointer arithmetic. The base attribute points at the owner (following the chain), and is None when the array owns its own buffer.

Which Operations View, Which Copy

OperationResultWhy
slice a[2:8], a[::2]viewcontiguous or strided window of the buffer
transpose a.T, swapaxesviewstrides permuted, data untouched
reshape (contiguous input)viewsame buffer, different shape
fancy a[[1,3,5]], boolean a[a > 3]copyarbitrary selection cannot be a window
astype, arithmetic a + 1copynew dtype or new values
copy(), asarray of a view (rare)copyexplicit or forced allocation
x = np.arange(10)
v = x[2:6]
k = x[[1, 3, 5]]
print(np.shares_memory(x, v))   # True  - the slice aliases x
print(np.shares_memory(x, k))   # False - fancy indexing copied

v[0] = 99
print(x[2])                     # 99 - the change passed through the view
k[0] = 77
print(x[1])                     # 1 - the copy did not

The test is np.shares_memory(a, b) - it answers the aliasing question directly, for any pair of arrays. The two surprises this catches: a "read-only" slice that still writes to the source, and a fancy index that you assumed aliased but did not. Both are one shares_memory call apart from an explanation.

Forcing Independence

s = x[2:8]              # a view - editing s edits x
s = x[2:8].copy()       # now independent
s[0] = 123
print(x[2])             # 99 - x is untouched, the copy absorbed the write

# ravel() views when possible, flatten() always copies
m = np.arange(12).reshape(3, 4)
print(np.shares_memory(m, m.ravel()))    # True  - flat view
print(np.shares_memory(m, m.flatten()))  # False - flat copy

# astype is a copy even for the "same" dtype in the common case
print(np.shares_memory(x, x.astype("int64")))  # False

copy() is the explicit independence operator - the answer whenever downstream code must not observe (or affect) the original. ravel versus flatten is the same choice in flat form: view-when-possible versus always-copy. And astype allocates a fresh buffer, which makes it a cheap "defensive copy with a type change" in one step.

The dtype Reinterpret View

f = np.array([1.0, 2.0], dtype="float64")
r = f.view("uint64")     # same bytes, read as 64-bit integers
print(r.dtype, r.shape)  # uint64 (2,) - one float64 per uint64

# the classic idiom: a wide int as two 32-bit ints
big = np.array([0x0000000100000002], dtype="int64")
print(big.view("int32")) # [2 1] - low word first in little-endian

a.view(dtype) reinterprets the raw bytes under a different dtype with no conversion - the element count changes to keep the total bytes equal. It is the tool for bit-level inspection (seeing the integer encoding of a float, packing/unpacking words) and must respect alignment: the new itemsize should divide the old one cleanly, or the view is not possible.

Copy-on-Write, the Modern Direction

NumPy 2.0 introduced an optional copy-on-write (CoW) mode: writes to a view allocate a private copy instead of mutating the shared buffer, making "I got a slice, I edited it" behave the way most people expect. The classic aliasing rules above still describe the default (reference) semantics in place today, but CoW is where the design is heading - the eventual model where a slice is a copy in behavior without paying the copy until it is actually written. Check the release notes for your version to see which mode your build uses.

Summary

Treat slicing, transposing, and contiguous reshapes as views - free windows over shared memory - and treat fancy/boolean indexing, astype, and arithmetic as copies. Confirm any uncertain aliasing with np.shares_memory in one call, force independence with copy() (or flatten, or an astype) the moment downstream code must not see the original, and use a.view(dtype) for byte-level reinterpretation. The aliasing rule explains the majority of "I changed the wrong array" bugs; the reflex is to know, at each step, whether you are holding a window or a buffer.