Skip to content

ndarray Object Model

Core Architecture

1. C-Contiguous Memory

The ndarray is NumPy's fundamental data structure, built around a contiguous block of memory with metadata:

import numpy as np

arr = np.arange(12).reshape(3, 4)
print(type(arr))  # <class 'numpy.ndarray'>
print(arr.flags)  # Shows C_CONTIGUOUS, OWNDATA, etc.

Key properties: - Data buffer: Raw memory block holding elements - Metadata: Shape, strides, dtype, flags - C-contiguous layout: Row-major ordering (default)

2. Memory Layout

NumPy stores arrays in row-major (C) or column-major (Fortran) order:

# C-contiguous (row-major)
arr_c = np.array([[1, 2, 3], [4, 5, 6]], order='C')
print(arr_c.flags['C_CONTIGUOUS'])  # True

# Fortran-contiguous (column-major)
arr_f = np.array([[1, 2, 3], [4, 5, 6]], order='F')
print(arr_f.flags['F_CONTIGUOUS'])  # True

Why it matters: - Cache locality affects performance - Interoperability with C/Fortran libraries - Stride calculations for slicing

3. Object Attributes

The ndarray exposes rich metadata:

arr = np.arange(24).reshape(2, 3, 4)

print(arr.shape)    # (2, 3, 4) - dimensions
print(arr.ndim)     # 3 - number of axes
print(arr.size)     # 24 - total elements
print(arr.dtype)    # dtype('int64') - element type
print(arr.itemsize) # 8 - bytes per element
print(arr.nbytes)   # 192 - total bytes
print(arr.strides)  # (96, 32, 8) - byte jumps per axis

Buffer Protocol

1. Memory Views

NumPy implements Python's buffer protocol for zero-copy data sharing:

arr = np.array([1, 2, 3, 4, 5])
mv = memoryview(arr)

print(mv.format)    # 'l' - long integer
print(mv.itemsize)  # 8
print(mv.ndim)      # 1
print(mv.shape)     # (5,)

2. Interoperability

Direct memory access enables interaction with compiled code:

import ctypes

arr = np.array([1, 2, 3], dtype=np.int32)
ptr = arr.ctypes.data_as(ctypes.POINTER(ctypes.c_int32))

# Can pass to C functions
print(ptr[0])  # 1

3. Zero-Copy Sharing

Multiple objects can reference the same data:

arr1 = np.arange(10)
arr2 = arr1  # Reference, not copy
arr2[0] = 999
print(arr1[0])  # 999 - shared memory

Construction Methods

1. From Sequences

# From list
arr = np.array([1, 2, 3])

# From nested list
arr2d = np.array([[1, 2], [3, 4]])

# With explicit dtype
arr_float = np.array([1, 2, 3], dtype=np.float64)

2. Factory Functions

# Zeros
zeros = np.zeros((3, 4))

# Ones
ones = np.ones((2, 3))

# Empty (uninitialized)
empty = np.empty((5,))

# Identity
identity = np.eye(3)

# Range
arange = np.arange(0, 10, 2)  # [0, 2, 4, 6, 8]

# Linspace
linspace = np.linspace(0, 1, 5)  # [0, 0.25, 0.5, 0.75, 1]

3. From Functions

# Using fromfunction
def f(i, j):
    return i + j

arr = np.fromfunction(f, (3, 3))
# [[0, 1, 2],
#  [1, 2, 3],
#  [2, 3, 4]]

Strides and Layout

1. Stride Calculation

Strides define byte jumps between elements:

arr = np.arange(24).reshape(2, 3, 4)
print(arr.strides)  # (96, 32, 8)

# Element at [i, j, k] is at:
# base_address + i*96 + j*32 + k*8

2. Transposition Effects

arr = np.arange(6).reshape(2, 3)
print(arr.strides)  # (24, 8) - C-contiguous

arr_t = arr.T
print(arr_t.strides)  # (8, 24) - not C-contiguous
print(arr_t.flags['C_CONTIGUOUS'])  # False

3. Reshape vs Ravel

arr = np.arange(12).reshape(3, 4)

# Ravel returns view if possible
flat = arr.ravel()
flat[0] = 999
print(arr[0, 0])  # 999 - view

# Flatten always copies
flat_copy = arr.flatten()
flat_copy[0] = -1
print(arr[0, 0])  # 999 - no change

Ownership and Flags

1. Data Ownership

arr = np.arange(10)
print(arr.flags['OWNDATA'])  # True

view = arr[::2]
print(view.flags['OWNDATA'])  # False - references arr's data

2. Writability

arr = np.arange(5)
print(arr.flags['WRITEABLE'])  # True

arr.flags.writeable = False
# arr[0] = 999  # ValueError: assignment destination is read-only

3. Alignment

arr = np.arange(10)
print(arr.flags['ALIGNED'])  # True

# Properly aligned for CPU SIMD operations

Performance Implications

1. Contiguous Access

import time

# C-contiguous access (fast)
arr = np.arange(10000000).reshape(10000, 1000)
start = time.time()
for i in range(arr.shape[0]):
    _ = arr[i, :].sum()
print(f"Row-wise: {time.time() - start:.3f}s")

# Column access (slower on C-contiguous)
start = time.time()
for j in range(arr.shape[1]):
    _ = arr[:, j].sum()
print(f"Column-wise: {time.time() - start:.3f}s")

2. View Semantics

# Views avoid copies
arr = np.arange(1000000)
view = arr[::2]  # No copy, instant

# But operations may need temporaries
result = view + 1  # Creates new array

3. SIMD Vectorization

Contiguous aligned arrays enable CPU SIMD:

arr = np.arange(1000000, dtype=np.float64)
# Operations vectorized across multiple elements simultaneously
result = arr * 2.0 + 1.0  # Single instruction, multiple data

Best Practices

1. Prefer Views

# ✅ GOOD - Use views
arr = np.arange(100)
subset = arr[10:20]  # View

# ❌ BAD - Unnecessary copy
subset = arr[10:20].copy()  # Only copy if needed

2. Check Contiguity

def ensure_contiguous(arr):
    if not arr.flags['C_CONTIGUOUS']:
        return np.ascontiguousarray(arr)
    return arr

3. Understand Ownership

arr = np.arange(10)
view = arr[::2]

# Modifying view affects original
view[0] = 999
assert arr[0] == 999

# Copy if independence needed
independent = arr[::2].copy()