詳解 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 統計二元語法中的頻次