numpy 数组 的 视图和副本
numpy 数组 的 视图
https://zhuanlan.zhihu.com/p/199615109?utm_id=0
https://finthon.com/numpy-arrayview-copy/
https://blog.csdn.net/weixin_44226181/article/details/128401161
==================================
在编程的过程中很可能会使用到原数组,这就涉及到视图和副本的概念,简单来说视图与副本是使用原数组的两种不同的方式。
import numpy as np
a = np.arange(4)
>>> print(a)
[0 1 2 3]
>>> print(a[1:3])
[1 2]
>>> print(a[[1, 2]])
[1 2]
# base属性
>>> print(a.base)
None
>>> print(a[1:3].base)
[0 1 2 3]
>>> print(a[[1, 2]].base)
None
# flags.owndata属性
>>> print(a.flags.owndata)
True
>>> print(a[1:3].flags.owndata)
False
>>> print(a[[1, 2]].flags.owndata)
True
虽然a[1:3]
和a[[1, 2]]
的输出结果都是 [1 2],但是从base
和flags.owndata
两个属性的输出结果来看,a[1:3]
和a[[1, 2]]
还是有一些差别的。a[1:3]
得到的是原数组的视图,而a[[1, 2]]
得到的是原数组的副本。那base
和flags.owndata
到底表示什么呢?
base与flags.owndata
下图是 Numpy 数组的内部结构组成。
其中可以分为数组数据结构信息区以及数据存储区。简单来说,数组数据结构信息区中有 Numpy 数组的形状(shape)以及数据类型(data-type)等信息,而数据存储区则是用于存储数组的数据,Numpy 数组中的数据可以指向其它数组中的数据,这样多个数组可以共用同一个数据:
ndarray.base
用于判断数组中的数据是否来自于别的数组;ndarray.flags.owndata
用于判断数组是否是数据的所有者;
就上例而言,a.base
和a[1, 2].base
返回的都是 None,说明a
和a[1, 2]
两个数组中的数据都来自于自己,不是来自别的数组。a.flags.owndata
和a[1, 2].flags.owndata
返回的都是True
,说明a
和a[1, 2]
两个数组都是数组中数据的所有者。
视图与副本
a[1:3]
得到的是原数组的视图,而a[[1, 2]]
得到的是原数组的副本。具体来说:
- 视图是对原数组的引用,或者自身没有数据,与原数组共享数据;
- 副本是对原数组的完整拷贝,虽然经过拷贝后的数组中的数据来自于原数组,但是它相对于原数组是独立的;
视图
Numpy 有两种方式能够产生原数组的视图:
- 对原数组的引用;
- 自身没有数据,与原数组共享数据;
import numpy as np
arr = np.arange(12)
cite_of_arr = arr
>>> print(arr)
[ 0 1 2 3 4 5 6 7 8 9 10 11]
>>> print(cite_of_arr)
[ 0 1 2 3 4 5 6 7 8 9 10 11]
>>> print(id(arr))
2517208831840
>>> print(id(cite_of_arr))
2517208831840
>>> print(arr.base)
None
>>> print(cite_of_arr.base)
None
>>> print(arr.flags.owndata)
True
>>> print(cite_of_arr.flags.owndata)
True
通过arr
和cite_of_arr
的内存地址(id函数)可以看出,其实两个数组是同一个,相当于多了一个名字而已,比较简单,这里不再赘述。view()
和reshape()
函数可以返回原数组的视图,此时的返回的数组自身没有数据,与原数组共享数据。
import numpy as np
arr = np.arange(12)
view_of_arr = arr.view()
view_of_arr.shape = (3, 4)
reshape_of_arr = arr.reshape(4, 3)
>>> print(arr.base)
None
>>> print(view_of_arr.base)
[ 0 1 2 3 4 5 6 7 8 9 10 11]
>>> print(reshape_of_arr.base)
[ 0 1 2 3 4 5 6 7 8 9 10 11]
>>> print(arr.flags.owndata)
True
>>> print(view_of_arr.flags.owndata)
False
>>> print(reshape_of_arr.flags.owndata)
False
其中view_of_arr
和reshape_of_arr
两个数组中的数据使用的都是arr
数组中的数据,这也是为什么view_of_arr.base
和reshape_of_arr.base
返回的都是 [ 0 1 2 3 4 5 6 7 8 9 10 11] 的原因,而由于arr
数组中的数据是它本身的,因此arr.base
返回 None。view_of_arr
和reshape_of_arr
两个数组都不是数据的所有者,只有arr
数组是数据的所有者,所以两个数组的flags.owndata
返回都是False
。
到底是不是共享数据,我们可以通过查看数组所占的内存地址进行验证。Numpy 数组所占的内存空间包含两个部分,数据结构信息区以及数据存储区,使用nbytes
属性可以查看数组中的数据所占的字节数。
>>> print(arr.nbytes)
48
>>> print(cite_of_arr.nbytes)
48
>>> print(view_of_arr.nbytes)
48
>>> print(reshape_of_arr.nbytes)
48
使用sys.getsizeof()
函数可以直接得到每个数组所占内存空间的大小。
>>> print(getsizeof(arr))
144
>>> print(getsizeof(cite_of_arr))
144
>>> print(getsizeof(view_of_arr))
112
>>> print(getsizeof(reshape_of_arr))
112
从输出结果可以发现,只有view_of_arr
和reshape_of_arr
两个数组所占的内存空间大小为 112,这是因为这两个数组自身没有数据,而使用的是原数组arr
的数据,而通过nbytes
属性知道了数据的内存大小为 48,这也从侧面证明了,view_of_arr
和reshape_of_arr
两个数组使用的是外部数据,而这外部数据的内存大小刚好是 48。
从图中可以看出,虽然view_of_arr
和reshape_of_arr
两个数组共用arr
数组的数据,但是由于它们有属于自己的数据结构信息区,因此可以将arr
数组中的原始数据以自己的方式进行表达(指定不同的 shape 以及 dtype 等)。
无论是对原数组的引用,还是自身没有数据,与原数组共享数据。这两种产生原数组视图的方式共享相同的数据,因此无论是修改原数组还是修改原数组的视图中的数据元素,共享数据的数组都会相对应的发生改变。
import numpy as np
arr = np.arange(12)
view_of_arr = arr.view()
view_of_arr.shape = (3, 4)
reshape_of_arr = arr.reshape(4, 3)
arr[1] = 100
view_of_arr[2] = 200
reshape_of_arr[3] = 300
>>> print(arr)
[ 0 100 2 3 4 5 6 7 200 300 300 300]
>>> print(view_of_arr)
[[ 0 100 2 3]
[ 4 5 6 7]
[200 300 300 300]]
>>> print(reshape_of_arr)
[[ 0 100 2]
[ 3 4 5]
[ 6 7 200]
[300 300 300]]
副本
副本是对原数组的完整拷贝,虽然经过拷贝后数组中的数据来自于原数组,但是它相对于原数组是独立的。使用copy()
方法可以返回原数组的副本。
import numpy as np
arr = np.arange(12)
copy_of_arr = arr.copy()
>>> print(arr)
[ 0 1 2 3 4 5 6 7 8 9 10 11]
>>> print(copy_of_arr)
[ 0 1 2 3 4 5 6 7 8 9 10 11]
>>> print(arr.base)
None
>>> print(copy_of_arr.base)
None
>>> print(arr.flags.owndata)
True
>>> print(copy_of_arr.flags.owndata)
True
arr
和copy_of_arr
两个数组的base
和flags.owndata
返回的值一样,这也可以验证虽然copy_of_arr
中的数据来源于原数组,但是它相对于原数组是独立的。
既然副本和原数组是相互独立的,改变副本或者原数组中的元素值,相对应的原数组和副本中的元素值并不会发生改变。
import numpy as np
arr = np.arange(12)
copy_of_arr = arr.copy()
arr[1] = 100
copy_of_arr[2] = 200
>>> print(arr)
[ 0 100 2 3 4 5 6 7 8 9 10 11]
>>> print(copy_of_arr)
[ 0 100 2 3 4 5 6 7 8 9 10 11]
小结
- 视图也被称为浅拷贝,而副本被称为深拷贝;
- 视图和副本的主要区别在于,修改原数组,视图会受到影响,而副本不会受到影响;
- 返回原数组视图和副本的常见操作:
- 视图:赋值引用,Numpy 的切片操作,调用
view()
函数,调用reshape()
函数; - 副本:Fancy Indexing(花式索引,例如
a[[1, 2]]
),调用copy()
函数; - 不能通过
id()
函数来区分视图和副本;
>>> print(id(arr) == id(cite_of_arr)) # 视图
True
>>> print(id(arr) == id(view_of_arr)) # 视图
False
>>> print(id(arr) == id(reshape_of_arr)) # 视图
False
>>> print(id(arr) == id(copy_of_arr)) # 副本
False
原创首发:公众号【AI机器学习与深度学习算法】