Skip to content

NumPy - Copies & Views

When working with NumPy arrays, certain operations return a ‘copy’ of the original array, while others return a ‘view’. Understanding the difference is crucial for efficient and bug-free code.

A Copy means the data is physically duplicated in another memory location. Changes to the copy do not affect the original array, and vice versa.

A View, on the other hand, provides a different perspective on the same memory content as the original array. Changes made through the view will affect the original array, and changes to the original may affect the view. Think of a view as a different lens to look at the same data.

Simple assignment (b = a) does not create a copy of the array or its data. It makes both variables refer to the exact same NumPy ndarray object in memory. You can verify this using Python’s built-in id() function, which returns a unique identifier for an object (similar to a memory address).

Because both variables point to the same object, any modification via one variable (like changing the shape or elements) will be immediately reflected when accessing the array through the other variable.

import numpy as np
a = np.arange(6)
print(f"Original array 'a':\n{a}")
print(f"ID of 'a': {id(a)}\n")
print("Assigning 'b = a'")
b = a
print(f"Array 'b':\n{b}")
print(f"ID of 'b': {id(b)} (Same as 'a')\n")
print("Changing shape of 'b' to (3, 2)")
b.shape = (3, 2)
print(f"New shape of 'b':\n{b}\n")
print("Shape of 'a' also changes:")
print(a)

Output:

Original array 'a':
[0 1 2 3 4 5]
ID of 'a': 140189398520688 # ID will vary
Assigning 'b = a'
Array 'b':
[0 1 2 3 4 5]
ID of 'b': 140189398520688 (Same as 'a')
Changing shape of 'b' to (3, 2)
New shape of 'b':
[[0 1]
[2 3]
[4 5]]
Shape of 'a' also changes:
[[0 1]
[2 3]
[4 5]]

A view creates a new ndarray object, but it shares the underlying data buffer with the original array. NumPy’s ndarray.view() method explicitly creates a view.

Key characteristics of a view:

  • The view has its own object identity (id(a) != id(b)).
  • The view shares data with the original. Modifying elements in the view changes the original, and vice versa.
  • The view can have different shape, strides, and data type (if compatible) without affecting the original array’s shape or dtype attributes. However, they still look at the same memory.

A very common way to get a view is through slicing.

import numpy as np
# Start with a 3x2 array
a = np.arange(6).reshape(3, 2)
print(f"Original array 'a':\n{a}")
print(f"ID of 'a': {id(a)}\n")
print("Creating view 'b = a.view()'")
b = a.view()
print(f"View 'b':\n{b}")
print(f"ID of 'b': {id(b)} (Different from 'a')\n")
# Change the shape of the view 'b'. This does NOT change 'a'.shape.
b.shape = (2, 3)
print(f"Shape of 'b' changed:\n{b}")
print(f"Shape of 'a' remains unchanged:\n{a}\n")
# Modify an element in the view 'b'
print("Modifying b[0, 0] = 99")
b[0, 0] = 99
print(f"View 'b' after modification:\n{b}")
print(f"Original array 'a' is also affected:\n{a}")

Output:

Original array 'a':
[[0 1]
[2 3]
[4 5]]
ID of 'a': 140189398521008 # ID will vary
Creating view 'b = a.view()'
View 'b':
[[0 1]
[2 3]
[4 5]]
ID of 'b': 140189398521152 # ID will vary (Different from 'a')
Shape of 'b' changed:
[[0 1 2]
[3 4 5]]
Shape of 'a' remains unchanged:
[[0 1]
[2 3]
[4 5]]
Modifying b[0, 0] = 99
View 'b' after modification:
[[99 1 2]
[ 3 4 5]]
Original array 'a' is also affected:
[[99 1]
[ 2 3]
[ 4 5]]

Basic slicing operations in NumPy typically return views. This is a performance optimization but can be surprising if you expect a copy.

import numpy as np
a = np.array([[10, 20, 30],
[40, 50, 60],
[70, 80, 90]])
print(f"Original array 'a':\n{a}\n")
print("Create a slice 's = a[:2, 1:]' (first 2 rows, columns from index 1 onwards)")
s = a[:2, 1:] # Selects [[20, 30], [50, 60]]
print(f"Slice 's':\n{s}\n")
print("Modify s[0, 0] = 999")
s[0, 0] = 999 # Modify the first element of the slice
print(f"Slice 's' after modification:\n{s}")
print(f"Original array 'a' is also modified:\n{a}")

Output:

Original array 'a':
[[ 10 20 30]
[ 40 50 60]
[ 70 80 90]]
Create a slice 's = a[:2, 1:]' (first 2 rows, columns from index 1 onwards)
Slice 's':
[[20 30]
[50 60]]
Modify s[0, 0] = 999
Slice 's' after modification:
[[999 30]
[ 50 60]]
Original array 'a' is also modified:
[[ 10 999 30]
[ 40 50 60]
[ 70 80 90]]

A deep copy, created using the ndarray.copy() method, duplicates both the array object and its data. The new array is completely independent of the original.

Use a deep copy when you need to modify a new array without affecting the original, or vice versa.

import numpy as np
a = np.array([[10, 10],
[ 2, 3],
[ 4, 5]])
print(f"Original array 'a':\n{a}")
print(f"ID of 'a': {id(a)}\n")
print("Create a deep copy 'b = a.copy()'")
b = a.copy()
print(f"Array 'b':\n{b}")
print(f"ID of 'b': {id(b)} (Different from 'a')\n")
# Check if 'b' shares memory with 'a'
# np.shares_memory() is a more direct way to check this
print(f"Does 'b' share memory with 'a'? {np.shares_memory(a, b)}")
# The 'is' operator checks object identity, which will be False for a copy.
print(f"Is 'b' the same object as 'a'? {b is a}\n")
print("Change the contents of 'b': b[0, 0] = 100")
b[0, 0] = 100
print(f"Modified array 'b':\n{b}\n")
print(f"Array 'a' remains unchanged:\n{a}")

Output:

Original array 'a':
[[10 10]
[ 2 3]
[ 4 5]]
ID of 'a': 140189398521568 # ID will vary
Create a deep copy 'b = a.copy()'
Array 'b':
[[10 10]
[ 2 3]
[ 4 5]]
ID of 'b': 140189398521712 # ID will vary (Different from 'a')
Does 'b' share memory with 'a'? False
Is 'b' the same object as 'a'? False
Change the contents of 'b': b[0, 0] = 100
Modified array 'b':
[[100 10]
[ 2 3]
[ 4 5]]
Array 'a' remains unchanged:
[[10 10]
[ 2 3]
[ 4 5]]

Key Takeaway: Be mindful of whether an operation returns a view or a copy, especially when modifying arrays. If you need independence, explicitly use .copy().