在上一节《Numpy数组的切片操作详解》中,我们提到了视图与副本,在本节我们将详细的阐述它们的概念,通过本节的知识的学习,你将从根本上理解 Numpy 数组的切片操作,和哪些操作是创建数组视图,哪些是创建数组副本,以及它们的作用。
在 Python 中 赋值是一种常见的操作,a=[1,3,4],则表明变量 a 指向列表 [1,2,3] 的内存地址,若 b=a 代表变量 b 指向相同的内存地址,这一点同样适应于 Numpy 数组,也就是说若 a、b 指向同一个数组,若数组的形状或者数据发生了变化,则 a、b都会发生相应的变化。如下所示:
In [15]: import numpy as np
...:
...: a = np.arange(6)
...: b = a
...: print('数组a:',a)
...: print('数组b:',b)
...:
...: print('a内存id:',id(a))
...: print('b内存id:',id(b))
...:
...: b.shape = (2,3)
...: print('修改b的形状:')
...: print(b)
...: print('a形状也发生改变:')
...: print(a)
数组a: [0 1 2 3 4 5]
数组b: [0 1 2 3 4 5]
a内存id: 10712232
b内存id: 10712232
修改b的形状:
[[0 1 2]
[3 4 5]]
a形状也发生改变:
[[0 1 2]
[3 4 5]]
ndarray 对象的 view() 方法创建可以创建视图数组,若使用该方法创建a 的视图 b ,则修改 b 的形状后,a 的形状不会发生改变,大家可以自己尝试一下。
这里的视图,并非 MVC 框架中的是视图,数组的视图是指与较大数组共享数据的较小数组(也可是原数组),既然是共享数据所示修改小数组中的数据会影响到大数组。这是一种简单的理解方式,通过视图可以访问、操作原有数据,但原有数据源不会产生拷贝,如果我们对视图进行修改,它会影响到原始数据。视图数组可以通过切片进行创建,还有一种方法就是可以是通过调用 ndarray 对象的 view() 方法。
In [1]: import numpy as np
In [2]: array=np.array([[1,3,5],[2,4,6],[7,9,10]])
In [3]: array
Out[3]:
array([[ 1, 3, 5],
[ 2, 4, 6],
[ 7, 9, 10]])
#查看内存id标识
In [4]: id(array)
Out[4]: 10711472
#选取第二行所有列生成arry1视图数组
In [5]: array1=arry[2,:]
In [6]: array1
Out[6]: array([ 7, 9, 10])
#查看内存id标识
In [7]: id(array1)
Out[7]: 74924584
#索引操作修改视图数组
In [8]: array1[1]=64
In [9]: array1
Out[9]: array([ 7, 64, 10])
#原数组被改变
In [10]: array
Out[10]:
array([[ 1, 3, 5],
[ 2, 4, 6],
[ 7, 64, 10]])
#切片赋值操作视图数组
In [11]: array1[:]=1314
In [12]: array1
Out[12]: array([1314, 1314, 1314])
In [13]: array
#原数组被改变
Out[13]:
array([[ 1, 3, 5],
[ 2, 4, 6],
[1314, 1314, 1314]])
从上述代码可以看出 Numpy 的切片操作会返回原数据的视图,切片产生的 array1 视图是原数组 array 的一部分,对视图的修改会直接反映到原数据中,但是可以发现 array 与 array1 的内存 id 并不相同,也就是视图虽然指向原数据,但是他们与赋值引用还是有区别的。
其实可以把它当做浅拷贝来理解,切片后产生的视图虽然有了新的内存 id,但是数组中的数据引用仍指向原数组的数据地址(即物理内存地址相同),也就是说它只是复制了一层外表而已。如果你熟悉 Python 中的浅拷贝和深拷贝理解起来会更加容易。
在 Numpy 中,数组的基础切片操作、ndarray. view() 方法、数组转置操作(即 ndarray.T)以及数组的变维操作(ndarray.reshape)都会得到一个视图,我们可以通过视图数组的 base 属性访问原数组。
In [21]: a=np.array([[1,3],[4,5]])
In [22]: b=a[0,:]
In [23]: b[0]=9
In [24]: b
Out[24]: array([9, 3])
In [25]: a
Out[25]:
array([[9, 3],
[4, 5]])
#使用base属性访问原数组
In [26]: b.base
Out[26]:
array([[9, 3],
[4, 5]])
副本是一个数据的完整拷贝,如果我们对副本进行修改,它不会影响到原始数据,因为物理内存不在同一位置,我们可以把副本理解为 Python 中的深拷贝,在 Numpy 中我们可以使用 ndarray.copy() 方法来生成副本。而在 Python 序列的切片操作时,调用 deepCopy() 函数进行深拷贝。下面我们看一下 Numpy 中如何生成副本。
In [1]: import numpy as np
In [2]: data=np.array([[1,3,5],[2,4,6],[7,9,10]])
#生成原数组
In [3]: data
Out[3]:
array([[ 1, 3, 5],
[ 2, 4, 6],
[ 7, 9, 10]])
#使用索引获取第一个维数组
In [4]: data[0]
Out[4]: array([1, 3, 5])
#创建副本data1深拷贝操作
In [5]: data1=data[0].copy()
#采用索引对改变原数组的值
In [6]: data[0]=11
In [7]: data
#原数组数据发生改变
Out[7]:
array([[11, 11, 11],
[ 2, 4, 6],
[ 7, 9, 10]])
In [8]: data[0]
Out[8]: array([11, 11, 11])
#对data[0]再赋值为data1
In [9]: data[0]=data1
#结果变为初始的data数组
In [10]: data
Out[10]:
array([[ 1, 3, 5],
[ 2, 4, 6],
[ 7, 9, 10]])
其实副本的操作不是很难理解,我们可以把它当做 Python 列表的深拷贝来理解,copy() 方法创建了 data1副本。因为经过拷贝的 data1 副本物理内存不同于原数组 data[0],所以如果对副本进行切片操作将不会影响到原数组数据。