Python / Numpy 副本与视图

Numpy 副本与视图

视图是指对数据的引用,通过该引用亦便可访问、操作原有数据,但原有数据不会产生拷贝。如果我们对视图进行修改,它会影响到原始数据,物理内存在同一位置。

副本是一个数据的完整的拷贝,如果我们对副本进行修改,它不会影响到原始数据,物理内存不在同一位置。

视图一般发生在:

  • Numpy 的切片操作返回原数据的视图;
  • 调用 ndarray 的 view() 函数产生一个视图。

副本一般发生在:

  • 在对 Python 序列进行切片操作时,同时调用 deepcopy() 函数;
  • 调用 ndarray (或其切片)的时候,同时调用 copy() 函数产生一个副本。

1. 直接赋值

1.1 ndarray 的赋值特性

对已经产生的 ndarray 对象,将该对象通过 = 方式再次赋值给其他变量,并不会创建数组对象的副本。即在该过程中产生的变量,都指向同一块物理内存地址。

案例

对于不同的变量,可以用 id() 函数来查看其对应的通用标识符,进而判断是否具有同一性。

a = np.arange(12)
print("数组a:", a)
print("数组a的id:", id(a))

打印结果为:

out:
    数组a: [ 0  1  2  3  4  5  6  7  8  9 10 11]
    数组a的id1383613133408

通过把 a 赋值给 b,创建一个新变量:

b = a
print("数组a:", b)
print("数组a的id:", id(b))

打印结果为:

数组a: [ 0  1  2  3  4  5  6  7  8  9 10 11]
数组a的id1383613133408

可以发现 a 和 b 的 id 完全一致,并且我们可以利用 is 判定符来佐证同一性的判定结论:

a is b
out:
    True

案例

在对 a 进行修改操作时,响应的效果也会同步显示在 b 变量中。

a.shape=(3,4)
a
out:
    array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])

修改 a 为二维数组,相应的,b也会产生同样的变化:

b
out:
    array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])

2 视图或浅拷贝

2.1 ndarray.view()

ndarray.view() 方会创建一个新的数组对象,该方法创建的新数组的维数更改不会更改原始数据的维数。

a = np.arange(6).reshape(3,2)
print("数组a:", a)
print("数组a的id:", id(a))

打印结果为:

数组a: [[0 1]
        [2 3]
        [4 5]]
数组a的id1383613212272

创建 a 的视图 b:

b = a.view()
print("视图b:", b)
print("视图b的id:", id(b))

打印结果为:

视图b: [[0 1]
 		[2 3]
 		[4 5]]
视图b的id1383613212672

可以看到,在视图产生的过程中,a 和 b 的 id 并不一致,这说明视图和直接赋值是不一样的。

案例

对视图 b 进行元素修改,该修改会同步反馈在变量 a 中:

b[0,0]=100
print("数组b:", b)
print("数组a:", a)

打印结果为:

数组b: [[100   1]
 		[  2   3]
 		[  4   5]]
数组a: [[100   1]
 		[  2   3]
 		[  4   5]]

案例

对视图 b 进行形状修改,并不影响到 a:

b.shape=2,3
print("数组b:", b)
print("数组a:", a)

打印结果为:

数组b: array([[100,   1,   2],
            [  3,   4,   5]])
数组a: [[100   1]
 		[  2   3]
 		[  4   5]]

2.2 切片

使用切片创建视图修改数组元素会影响到原始数组。

arr = np.arange(12)
print ("数组arr:", arr)

创建的 arr 数组为:

数组arr: [ 0  1  2  3  4  5  6  7  8  9 10 11]

分别通过切片产生 a 和 b:

a=arr[3:]
b=arr[3:]
print("修改前的切片a:", a)
print("修改前的切片b:", b)

切片结果 a 和 b 为:

修改前的切片a: [ 3  4  5  6  7  8  9 10 11]
修改前的切片b: [ 3  4  5  6  7  8  9 10 11]

分别改变切片 a 和 b 中的元素:

a[1]=123
b[2]=234
print("修改后的切片a:", a)
print("修改后的切片b:", b)

修改后的 a 和 b 为:

修改后的切片a: [  3 123 234   6   7   8   9  10  11]
修改后的切片b: [  3 123 234   6   7   8   9  10  11]

可以看到,对 a 和 b 所做的修改,都同时出现了。这说明切片直接是互相影响的。

print("修改后的原数组arr:", arr)

打印结果为:

修改后的原数组arr: [  0   1   2   3 123 234   6   7   8   9  10  11]

综合看下来,我们可以发现:变量 a,b 都是 arr 的一部分视图,对视图的修改会直接反映到原数据和相关切片中。

3 副本或深拷贝

3.1 ndarray.copy()

ndarray.copy() 函数创建一个副本。 对副本数据进行修改,不会影响到原始数据,它们物理内存不在同一位置。

案例

创建数组 a,并产生 a 的副本,记为 b:

a = np.array([[0,1], [2,3], [4,5]])
b = a.copy()

判断 a 和 b 是否具有同一性:

b is a
out:
    False

可以看到,a 和 b 互相独立,这和赋值显然不同。

对副本进行修改,观察原始数组:

b[0,0]=100
print("修改后的数组b:", b)
print("原始数组a:", a)

打印结果为:

修改后的数组b: [[100   1]
			 [  2   3]
			 [  4   5]]
原始数组a: [[0 1]
		  [2 3]
		  [4 5]]

可以发现,副本产生的变化,并不会对原始数组产生影响。

4. 小结

本小节讲解了视图和副本的概念和区别。副本是对原始数组的完整拷贝,二者互相独立,并不互相影响,但是物理内存的开销会加倍。而视图(切片)是对原始数据的一种映射,物理内存的开销相对小一些;对视图(切片)的元素更改,会相应地反映到原始数组中,这是二者最大的区别。