系列文章地址
ndarray 物件的內部機理
在前面的內容中,我們已經詳細講述了 ndarray
的使用,在本章的開始部分,我們來聊一聊 ndarray
的內部機理,以便更好的理解後續的內容。
1、ndarray 的組成
ndarray 與陣列不同,它不僅僅包含資料資訊,還包括其他描述資訊。ndarray 內部由以下內容組成:
- 資料指標:一個指向實際資料的指標。
- 資料型別(dtype):描述了每個元素所佔位元組數。
- 維度(shape):一個表示陣列形狀的元組。
- 跨度(strides):一個表示從當前維度前進道下一維度的當前位置所需要“跨過”的位元組數。
NumPy 中,資料儲存在一個均勻連續的記憶體塊中,可以這麼理解,NumPy 將多維陣列在內部以一維陣列的方式儲存,我們只要知道了每個元素所佔的位元組數(dtype)以及每個維度中元素的個數(shape),就可以快速定位到任意維度的任意一個元素。
dtype 及 shape 前文中已經有詳細描述,這裡我們來講下 strides。
示例
ls = [[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]],
[[13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]]
a = np.array(ls, dtype=int)
print(a)
print(a.strides)
複製程式碼
輸出:
[[[ 1 2 3 4]
[ 5 6 7 8]
[ 9 10 11 12]]
[[13 14 15 16]
[17 18 19 20]
[21 22 23 24]]]
(48, 16, 4)
複製程式碼
上例中,我們定義了一個三維陣列,dtype 為 int,int 佔 4個位元組。 第一維度,從元素 1 到元素 13,間隔 12 個元素,總位元組數為 48; 第二維度,從元素 1 到元素 5,間隔 4 個元素,總位元組數為 16; 第三維度,從元素 1 到元素 2,間隔 1 個元素,總位元組數為 4。 所以跨度為(48, 16, 4)。
普通迭代
ndarray 的普通迭代跟 Python 及其他語言中的迭代方式無異,N 維陣列,就要用 N 層的 for
迴圈。
示例:
import numpy as np
ls = [[1, 2], [3, 4], [5, 6]]
a = np.array(ls, dtype=int)
for row in a:
for cell in row:
print(cell)
複製程式碼
輸出:
1
2
3
4
5
6
複製程式碼
上例中,row
的資料型別依然是 numpy.ndarray
,而 cell
的資料型別是 numpy.int32
。
nditer 多維迭代器
NumPy 提供了一個高效的多維迭代器物件:nditer 用於迭代陣列。在普通方式的迭代中,N 維陣列,就要用 N 層的 for
迴圈。但是使用 nditer
迭代器,一個 for
迴圈就能遍歷整個陣列。(因為 ndarray 在記憶體中是連續的,連續記憶體不就相當於是一維陣列嗎?遍歷一維陣列當然只需要一個 for
迴圈就行了。)
1、基本示例
例一:
ls = [[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]],
[[13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]]
a = np.array(ls, dtype=int)
for x in np.nditer(a):
print(x, end=", ")
複製程式碼
輸出:
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
複製程式碼
2、order 引數:指定訪問元素的順序
建立 ndarray 陣列時,可以通過 order 引數指定元素的順序,按行還是按列,這是什麼意思呢?來看下面的示例:
例二:
ls = [[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]],
[[13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]]
a = np.array(ls, dtype=int, order='F')
for x in np.nditer(a):
print(x, end=", ")
複製程式碼
輸出:
1, 13, 5, 17, 9, 21, 2, 14, 6, 18, 10, 22, 3, 15, 7, 19, 11, 23, 4, 16, 8, 20, 12, 24,
複製程式碼
nditer
預設以記憶體中元素的順序(order='K')訪問元素,對比例一可見,建立 ndarray 時,指定不同的順序將影響元素在記憶體中的位置。
例三:
nditer
也可以指定使用某種順序遍歷。
ls = [[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]],
[[13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]]
a = np.array(ls, dtype=int, order='F')
for x in np.nditer(a, order='C'):
print(x, end=", ")
複製程式碼
輸出:
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
複製程式碼
行主順序(
order='C'
)和列主順序(order='F'
),參看 en.wikipedia.org/wiki/Row-_a…。例一是行主順序,例二是列主順序,如果將 ndarray 陣列想象成一棵樹,那麼會發現,行主順序就是深度優先,而列主順序就是廣度優先。NumPy 中之所以要分行主順序和列主順序,主要是為了在矩陣運算中提高效能,順序訪問比非順序訪問快幾個數量級。(矩陣運算將會在後面的章節中講到)
3、op_flags 引數:迭代時修改元素的值
預設情況下,nditer 將視待迭代遍歷的陣列為只讀物件(readonly),為了在遍歷陣列的同時,實現對陣列元素值得修改,必須指定 op_flags
引數為 readwrite 或者 writeonly 的模式。
例四:
import numpy as np
a = np.arange(5)
for x in np.nditer(a, op_flags=['readwrite']):
x[...] = 2 * x
print(a)
複製程式碼
輸出:
[0 1 2 3 4]
複製程式碼
4、flags 引數
flags
引數需要傳入一個陣列或元組,既然引數型別是陣列,我原本以為可以傳入多個值的,但是,就下面介紹的 4 種常用選項,我試了,不能傳多個,例如 flags=['f_index', 'external_loop']
,執行報錯。
(1)使用外部迴圈:external_loop
將一維的最內層的迴圈轉移到外部迴圈迭代器,使得 NumPy 的向量化操作在處理更大規模資料時變得更有效率。
簡單來說,當指定 flags=['external_loop']
時,將返回一維陣列而並非單個元素。具體來說,當 ndarray 的順序和遍歷的順序一致時,將所有元素組成一個一維陣列返回;當 ndarray 的順序和遍歷的順序不一致時,返回每次遍歷的一維陣列(這句話特別不好描述,看例子就清楚了)。
例五:
import numpy as np
ls = [[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]],
[[13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]]
a = np.array(ls, dtype=int, order='C')
for x in np.nditer(a, flags=['external_loop'], order='C'):
print(x,)
複製程式碼
輸出:
[ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24]
複製程式碼
例六:
b = np.array(ls, dtype=int, order='F')
for x in np.nditer(b, flags=['external_loop'], order='C'):
print(x,)
複製程式碼
輸出:
[1 2 3 4]
[5 6 7 8]
[ 9 10 11 12]
[13 14 15 16]
[17 18 19 20]
[21 22 23 24]
複製程式碼
(2)追蹤索引:c_index、f_index、multi_index
例七:
import numpy as np
a = np.arange(6).reshape(2, 3)
it = np.nditer(a, flags=['f_index'])
while not it.finished:
print("%d <%d>" % (it[0], it.index))
it.iternext()
複製程式碼
輸出:
0 <0>
1 <2>
2 <4>
3 <1>
4 <3>
5 <5>
複製程式碼
這裡索引之所以是這樣的順序,因為我們選擇的是列索引(f_index)。直觀的感受看下圖:
遍歷元素的順序是由
order
引數決定的,而行索引(c_index)和列索引(f_index)不論如何指定,並不會影響元素返回的順序。它們僅表示在當前記憶體順序下,如果按行/列順序返回,各個元素的下標應該是多少。
例八:
import numpy as np
a = np.arange(6).reshape(2, 3)
it = np.nditer(a, flags=['multi_index'])
while not it.finished:
print("%d <%s>" % (it[0], it.multi_index))
it.iternext()
複製程式碼
輸出:
0 <(0, 0)>
1 <(0, 1)>
2 <(0, 2)>
3 <(1, 0)>
4 <(1, 1)>
5 <(1, 2)>
複製程式碼
5、同時迭代多個陣列
說到同時遍歷多個陣列,第一反應會想到 zip 函式,而在 nditer 中不需要。
例九:
a = np.array([1, 2, 3], dtype=int, order='C')
b = np.array([11, 12, 13], dtype=int, order='C')
for x, y in np.nditer([a, b]):
print(x, y)
複製程式碼
輸出:
1 11
2 12
3 13
複製程式碼
其他函式
1、flatten函式
flatten
函式將多維 ndarray 展開成一維 ndarray 返回。
語法:
flatten(order='C')
複製程式碼
示例:
import numpy as np
a = np.array([[1, 2, 3], [4, 5, 6]], dtype=int, order='C')
b = a.flatten()
print(b)
print(type(b))
複製程式碼
輸出:
[1 2 3 4 5 6]
<class 'numpy.ndarray'>
複製程式碼
2、flat
flat
返回一個迭代器,可以遍歷陣列中的每一個元素。
import numpy as np
a = np.array([[1, 2, 3], [4, 5, 6]], dtype=int, order='C')
for b in a.flat:
print(b)
print(type(a.flat))
複製程式碼
輸出:
1
2
3
4
5
6
<class 'numpy.flatiter'>
複製程式碼