详解 Numpy 中的视图和副本

  • 2020 年 11 月 3 日
  • AI
关注”AI机器学习与深度学习算法“公众号
选择”星标“公众号,原创干货,第一时间送达

在编程的过程中很可能会使用到原数组,这就涉及到视图和副本的概念,简单来说视图与副本是使用原数组的两种不同的方式。

import numpy as np

a = np.arange(4)

>>> print(a)
[0 1 2 3]
>>> print(a[1:3])
[1 2]
>>> print(a[[12]])
[1 2]

# base属性
>>> print(a.base)
None
>>> print(a[1:3].base)
[0 1 2 3]
>>> print(a[[12]].base)
None

# flags.owndata属性
>>> print(a.flags.owndata)
True
>>> print(a[1:3].flags.owndata)
False
>>> print(a[[12]].flags.owndata)
True

虽然a[1:3]a[[1, 2]]的输出结果都是 [1 2],但是从baseflags.owndata两个属性的输出结果来看,a[1:3]a[[1, 2]]还是有一些差别的。a[1:3]得到的是原数组的视图,而a[[1, 2]]得到的是原数组的副本。那baseflags.owndata到底表示什么呢?

base与flags.owndata

下图是 Numpy 数组的内部结构组成。

其中可以分为数组数据结构信息区以及数据存储区。简单来说,数组数据结构信息区中有 Numpy 数组的形状(shape)以及数据类型(data-type)等信息,而数据存储区则是用于存储数组的数据,「Numpy 数组中的数据可以指向其它数组中的数据,这样多个数组可以共用同一个数据:」

  • ndarray.base用于判断数组中的数据是否来自于别的数组;
  • ndarray.flags.owndata用于判断数组是否是数据的所有者;

就上例而言,a.basea[1, 2].base返回的都是 None,说明aa[1, 2]两个数组中的数据都来自于自己,不是来自别的数组。a.flags.owndataa[1, 2].flags.owndata返回的都是True,说明aa[1, 2]两个数组都是数组中数据的所有者。

视图与副本

a[1:3]得到的是原数组的视图,而a[[1, 2]]得到的是原数组的副本。具体来说:

  • 视图是对原数组的引用,或者自身没有数据,与原数组共享数据;
  • 副本是对原数组的完整拷贝,虽然经过拷贝后的数组中的数据来自于原数组,但是它相对于原数组是独立的;

视图

Numpy 有两种方式能够产生原数组的视图:

  1. 对原数组的引用;
  2. 自身没有数据,与原数组共享数据;
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

通过arrcite_of_arr的内存地址(id函数)可以看出,其实两个数组是同一个,相当于多了一个名字而已,比较简单,这里不再赘述。view()reshape()函数可以返回原数组的视图,此时的返回的数组自身没有数据,与原数组共享数据。

import numpy as np

arr = np.arange(12)

view_of_arr = arr.view()
view_of_arr.shape = (34)

reshape_of_arr = arr.reshape(43)

>>> 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_arrreshape_of_arr两个数组中的数据使用的都是arr数组中的数据,这也是为什么view_of_arr.basereshape_of_arr.base返回的都是 [ 0  1  2  3  4  5  6  7  8  9 10 11] 的原因,而由于arr数组中的数据是它本身的,因此arr.base返回 None。view_of_arrreshape_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_arrreshape_of_arr两个数组所占的内存空间大小为 112,这是因为这两个数组自身没有数据,而使用的是原数组arr的数据,而通过nbytes属性知道了数据的内存大小为 48,这也从侧面证明了,view_of_arrreshape_of_arr两个数组使用的是外部数据,而这外部数据的内存大小刚好是 48。

「从图中可以看出,虽然view_of_arrreshape_of_arr两个数组共用arr数组的数据,但是由于它们有属于自己的数据结构信息区,因此可以将arr数组中的原始数据以自己的方式进行表达(指定不同的 shape 以及 dtype 等)。」

无论是对原数组的引用,还是自身没有数据,与原数组共享数据。这两种产生原数组视图的方式共享相同的数据,因此无论是修改原数组还是修改原数组的视图中的数据元素,共享数据的数组都会相对应的发生改变。

import numpy as np

arr = np.arange(12)

view_of_arr = arr.view()
view_of_arr.shape = (34)

reshape_of_arr = arr.reshape(43)

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

arrcopy_of_arr两个数组的baseflags.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


【数据分析】Numpy中常用随机函数的总结

【数据分析】Numpy中的两个乱序函数

【机器学习】详解训练集、测试集以及验证集

【资源分享】电子书 | 廖雪峰 Python3 教程(带标签)

【自然语言处理】使用 HanLP 统计二元语法中的频次