NumPy 超詳細教程(3):ndarray 的內部機理及高級迭代

 系列文章地址

 


 

ndarray 對象的內部機理

在前面的內容中,我們已經詳細講述了 ndarray 的使用,在本章的開始部分,我們來聊一聊 ndarray 的內部機理,以便更好的理解後續的內容。

1、ndarray 的組成

ndarray 與數組不同,它不僅僅包含數據信息,還包括其他描述信息。ndarray 內部由以下內容組成:

  • 數據指針:一個指向實際數據的指針。
  • 數據類型(dtype):描述了每個元素所佔字節數。
  • 維度(shape):一個表示數組形狀的元組。
  • 跨度(strides):一個表示從當前維度前進道下一維度的當前位置所需要“跨過”的字節數。

NumPy 中,數據存儲在一個均勻連續的內存塊中,可以這麼理解,NumPy 將多維數組在內部以一維數組的方式存儲,我們只要知道了每個元素所佔的字節數(dtype)以及每個維度中元素的個數(shape),就可以快速定位到任意維度的任意一個元素。

dtypeshape 前文中已經有詳細描述,這裏我們來講下 strides

示例

ls = [[[1234], [5678], [9101112]], 
      [[13141516], [17181920], [21222324]]]
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]]]
(48164)

上例中,我們定義了一個三維數組,dtypeintint 佔 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 = [[12], [34], [56]]
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 = [[[1234], [5678], [9101112]],
      [[13141516], [17181920], [21222324]]]
a = np.array(ls, dtype=int)
for x in np.nditer(a):
    print(x, end=", ")

輸出:

123456789101112131415161718192021222324

2、order 參數:指定訪問元素的順序

創建 ndarray 數組時,可以通過 order 參數指定元素的順序,按行還是按列,這是什麼意思呢?來看下面的示例:

例二:

ls = [[[1234], [5678], [9101112]],
      [[13141516], [17181920], [21222324]]]
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 = [[[1234], [5678], [9101112]],
      [[13141516], [17181920], [21222324]]]
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'),參看 https://en.wikipedia.org/wiki/Row-_and_column-major_order。例一是行主順序,例二是列主順序,如果將 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 = [[[1234], [5678], [9101112]],
      [[13141516], [17181920], [21222324]]]
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(23)
it = np.nditer(a, flags=['f_index'])

while not it.finished:
    print("%d <%d>" % (it[0], it.index))
    it.iternext()

輸出:

<0>
<2>
<4>
<1>
<3>
<5>

這裏索引之所以是這樣的順序,因爲我們選擇的是列索引(f_index)。直觀的感受看下圖:

遍歷元素的順序是由 order 參數決定的,而行索引(c_index)和列索引(f_index)不論如何指定,並不會影響元素返回的順序。它們僅表示在當前內存順序下,如果按行/列順序返回,各個元素的下標應該是多少。

例八:

import numpy as np

a = np.arange(6).reshape(23)
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, 2)>
<(1, 0)>
<(1, 1)>
<(1, 2)>

5、同時迭代多個數組

說到同時遍歷多個數組,第一反應會想到 zip 函數,而在 nditer 中不需要。

例九:

a = np.array([123], dtype=int, order='C')
b = np.array([111213], 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([[123], [456]], 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([[123], [456]], dtype=int, order='C')
for b in a.flat:
    print(b)
print(type(a.flat))

輸出:

1
2
3
4
5
6
<class 'numpy.flatiter'>

歡迎關注我的公衆號
大齡碼農的Python之路
掃碼關注我的個人公衆號
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章