在上一节《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],所以如果对副本进行切片操作将不会影响到原数组数据。