博客:博客園 | CSDN | blog
目錄- ndarray是什么
- ndarray的設計哲學
- ndarray的記憶體布局
- 為什么可以這樣設計
- 小結
- 參考
本文的主要目的在于理解numpy.ndarray的記憶體結構及其背后的設計哲學,
ndarray是什么
NumPy provides an N-dimensional array type, the ndarray, which describes a collection of “items” of the same type. The items can be indexed using for example N integers.
—— from https://docs.scipy.org/doc/numpy-1.17.0/reference/arrays.html
ndarray是numpy中的多維陣列,陣列中的元素具有相同的型別,且可以被索引,
如下所示:
>>> import numpy as np
>>> a = np.array([[0,1,2,3],[4,5,6,7],[8,9,10,11]])
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> type(a)
<class 'numpy.ndarray'>
>>> a.dtype
dtype('int32')
>>> a[1,2]
6
>>> a[:,1:3]
array([[ 1, 2],
[ 5, 6],
[ 9, 10]])
>>> a.ndim
2
>>> a.shape
(3, 4)
>>> a.strides
(16, 4)
注:np.array并不是類,而是用于創建np.ndarray物件的其中一個函式,numpy中多維陣列的類為np.ndarray,
ndarray的設計哲學
ndarray的設計哲學在于資料存盤與其解釋方式的分離,或者說copy和view的分離,讓盡可能多的操作發生在解釋方式上(view上),而盡量少地操作實際存盤資料的記憶體區域,
如下所示,像reshape操作回傳的新物件b,a和b的shape不同,但是兩者共享同一個資料block,c=b.T,c是b的轉置,但兩者仍共享同一個資料block,資料并沒有發生變化,發生變化的只是資料的解釋方式,
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> b = a.reshape(4, 3)
>>> b
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11]])
# reshape操作產生的是view視圖,只是對資料的解釋方式發生變化,資料物理地址相同
>>> a.ctypes.data
80831392
>>> b.ctypes.data
80831392
>>> id(a) == id(b)
false
# 資料在記憶體中連續存盤
>>> from ctypes import string_at
>>> string_at(b.ctypes.data, b.nbytes).hex()
'000000000100000002000000030000000400000005000000060000000700000008000000090000000a0000000b000000'
# b的轉置c,c仍共享相同的資料block,只改變了資料的解釋方式,“以列優先的方式解釋行優先的存盤”
>>> c = b.T
>>> c
array([[ 0, 3, 6, 9],
[ 1, 4, 7, 10],
[ 2, 4, 8, 11]])
>>> c.ctypes.data
80831392
>>> string_at(c.ctypes.data, c.nbytes).hex()
'000000000100000002000000030000000400000005000000060000000700000008000000090000000a0000000b000000'
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
# copy會復制一份新的資料,其物理地址位于不同的區域
>>> c = b.copy()
>>> c
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11]])
>>> c.ctypes.data
80831456
>>> string_at(c.ctypes.data, c.nbytes).hex()
'000000000100000002000000030000000400000005000000060000000700000008000000090000000a0000000b000000'
# slice操作產生的也是view視圖,仍指向原來資料block中的物理地址
>>> d = b[1:3, :]
>>> d
array([[3, 4, 5],
[6, 7, 8]])
>>> d.ctypes.data
80831404
>>> print('data buff address from {0} to {1}'.format(b.ctypes.data, b.ctypes.data + b.nbytes))
data buff address from 80831392 to 80831440
副本是一個資料的完整的拷貝,如果我們對副本進行修改,它不會影響到原始資料,物理記憶體不在同一位置,
視圖是資料的一個別稱或參考,通過該別稱或參考亦便可訪問、操作原有資料,但原有資料不會產生拷貝,如果我們對視圖進行修改,它會影響到原始資料,物理記憶體在同一位置,
視圖一般發生在:
- 1、numpy 的切片操作回傳原資料的視圖,
- 2、呼叫 ndarray 的 view() 函式產生一個視圖,
副本一般發生在:
- Python 序列的切片操作,呼叫deepCopy()函式,
- 呼叫 ndarray 的 copy() 函式產生一個副本,
—— from NumPy 副本和視圖
view機制的好處顯而易見,省記憶體,同時速度快,
ndarray的記憶體布局
NumPy arrays consist of two major components, the raw array data (from now on, referred to as the data buffer), and the information about the raw array data. The data buffer is typically what people think of as arrays in C or Fortran, a contiguous (and fixed) block of memory containing fixed sized data items. NumPy also contains a significant set of data that describes how to interpret the data in the data buffer.
—— from NumPy internals
ndarray的記憶體布局示意圖如下:

可大致劃分成2部分——對應設計哲學中的資料部分和解釋方式:
- raw array data:為一個連續的memory block,存盤著原始資料,類似C或Fortran中的陣列,連續存盤
- metadata:是對上面記憶體塊的解釋方式
metadata都包含哪些資訊呢?
dtype:資料型別,指示了每個資料占用多少個位元組,這幾個位元組怎么解釋,比如int32、float32等;ndim:有多少維;shape:每維上的數量;strides:維間距,即到達當前維下一個相鄰資料需要前進的位元組數,因考慮記憶體對齊,不一定為每個資料占用位元組數的整數倍;
上面4個資訊構成了ndarray的indexing schema,即如何索引到指定位置的資料,以及這個資料該怎么解釋,
除此之外的資訊還有:位元組序(大端小端)、讀寫權限、C-order(行優先存盤) or Fortran-order(列優先存盤)等,如下所示,
>>> a.flags
C_CONTIGUOUS : True
F_CONTIGUOUS : False
OWNDATA : True
WRITEABLE : True
ALIGNED : True
WRITEBACKIFCOPY : False
UPDATEIFCOPY : False
ndarray的底層是C和Fortran實作,上面的屬性可以在其原始碼中找到對應,具體可見PyArrayObject和PyArray_Descr等結構體,
為什么可以這樣設計
為什么ndarray可以這樣設計?
因為ndarray是為矩陣運算服務的,ndarray中的所有資料都是同一種型別,比如int32、float64等,每個資料占用的位元組數相同、解釋方式也相同,所以可以稠密地排列在一起,在取出時根據dtype現copy一份資料組裝成scalar物件輸出,這樣極大地節省了空間,scalar物件中除了資料之外的域沒必要重復存盤,同時因為連續記憶體的原因,可以按秩訪問,速度也要快得多,

>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> a[1,1]
5
>>> i,j = a[1,1], a[1,1]
# i和j為不同的物件,訪問一次就“組裝一個”物件
>>> id(i)
102575536
>>> id(j)
102575584
>>> a[1,1] = 4
>>> i
5
>>> j
5
>>> a
array([[ 0, 1, 2, 3],
[ 4, 4, 6, 7],
[ 8, 9, 10, 11]])
# isinstance(val, np.generic) will return True if val is an array scalar object. Alternatively, what kind of array scalar is present can be determined using other members of the data type hierarchy.
>> isinstance(i, np.generic)
True
這里,可以將ndarray與python中的list對比一下,list可以容納不同型別的物件,像string、int、tuple等都可以放在一個list里,所以list中存放的是物件的參考,再通過參考找到具體的物件,這些物件所在的物理地址并不是連續的,如下所示

所以相對ndarray,list訪問到資料需要多跳轉1次,list只能做到對物件參考的按秩訪問,對具體的資料并不是按秩訪問,所以效率上ndarray比list要快得多,空間上,因為ndarray只把資料緊密存盤,而list需要把每個物件的所有域值都存下來,所以ndarray比list要更省空間,
小結
下面小結一下:
ndarray的設計哲學在于資料與其解釋方式的分離,讓絕大部分多維陣列操作只發生在解釋方式上;ndarray中的資料在物理記憶體上連續存盤,在讀取時根據dtype現組裝成物件輸出,可以按秩訪問,效率高省空間;- 之所以能這樣實作,在于
ndarray是為矩陣運算服務的,所有資料單元都是同種型別,
參考
- Array objects
- NumPy internals
- NumPy C Code Explanations
- Python Types and C-Structures
- How is the memory allocated for numpy arrays in python?
- NumPy 副本和視圖
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/197784.html
標籤:Python
上一篇:Python3(九) 閉包
