詳解 Numpy 中的視圖和副本
- 2020 年 11 月 3 日
- AI
在編程的過程中很可能會使用到原數組,這就涉及到視圖和副本的概念,簡單來說視圖與副本是使用原數組的兩種不同的方式。
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
【資源分享】電子書 | 廖雪峰 Python3 教程(帶標籤)