緩衝區協議¶
Python 中某些物件封裝了對底層記憶體陣列或 **緩衝器** 的訪問。這些物件包括內建的 bytes
和 bytearray
,以及一些擴充套件型別,如 array.array
。第三方庫可能會為影像處理或數值分析等特殊目的定義自己的型別。
雖然這些型別都有各自的語義,但它們都具有由可能較大的記憶體緩衝區支援的共同特徵。因此,在某些情況下,希望直接訪問該緩衝區而無需中間複製。
Python 在 C 和 Python 級別透過 緩衝區協議 提供了這種機制。該協議分為兩方面:
在生產者方面,型別可以匯出一個“緩衝區介面”,允許該型別的物件公開有關其底層緩衝區的資訊。此介面在 緩衝區物件結構 部分中描述;對於 Python,請參閱 模擬緩衝區型別。
在消費者方面,有多種方法可以獲取指向物件原始底層資料(例如方法引數)的指標。對於 Python,請參閱
memoryview
。
像 bytes
和 bytearray
這樣的簡單物件以位元組形式公開其底層緩衝區。其他形式也是可能的;例如,array.array
公開的元素可以是多位元組值。
緩衝區介面的一個消費者示例是檔案物件的 write()
方法:任何可以透過緩衝區介面匯出位元組序列的物件都可以寫入檔案。雖然 write()
只需要對其傳入物件的內部內容進行只讀訪問,但其他方法(如 readinto()
)需要對其引數的內容進行寫入訪問。緩衝區介面允許物件選擇性地允許或拒絕匯出讀寫和只讀緩衝區。
緩衝區介面的消費者有兩種方式獲取目標物件的緩衝區:
呼叫具有正確引數的
PyObject_GetBuffer()
;呼叫
PyArg_ParseTuple()
(或其姊妹函式之一),並使用y*
、w*
或s*
中的一個 格式程式碼。
在這兩種情況下,當不再需要緩衝區時,必須呼叫 PyBuffer_Release()
。否則可能會導致各種問題,例如資源洩漏。
3.12 版本新增:緩衝區協議現在可在 Python 中訪問,請參閱 模擬緩衝區型別 和 memoryview
。
緩衝區結構¶
緩衝區結構(或簡稱“緩衝區”)是一種將其他物件的二進位制資料公開給 Python 程式設計師的有用方式。它們還可以用作零複製切片機制。利用其引用記憶體塊的能力,可以非常容易地將任何資料公開給 Python 程式設計師。記憶體可以是 C 擴充套件中一個大的、常量陣列,也可以是在傳遞給作業系統庫之前用於操作的原始記憶體塊,或者可以用於以其原生的記憶體格式傳遞結構化資料。
與 Python 直譯器公開的大多數資料型別相反,緩衝區不是 PyObject
指標,而是簡單的 C 結構體。這使得它們可以非常簡單地建立和複製。當需要一個通用緩衝區包裝器時,可以建立一個 memoryview 物件。
有關如何編寫匯出物件的簡短說明,請參閱 緩衝區物件結構。有關獲取緩衝區的資訊,請參閱 PyObject_GetBuffer()
。
-
type Py_buffer¶
- 自 3.11 版本以來,它是 穩定 ABI 的一部分(包括所有成員)。
-
void *buf¶
指向緩衝區欄位描述的邏輯結構開頭的指標。這可以是匯出器底層物理記憶體塊中的任何位置。例如,當
strides
為負時,該值可能指向記憶體塊的末尾。對於 連續 陣列,該值指向記憶體塊的開頭。
-
PyObject *obj¶
對匯出物件的新引用。該引用由消費者擁有,並由
PyBuffer_Release()
自動釋放(即引用計數遞減)並設定為NULL
。該欄位等同於任何標準 C-API 函式的返回值。作為特例,對於由
PyMemoryView_FromBuffer()
或PyBuffer_FillInfo()
包裝的 *臨時* 緩衝區,此欄位為NULL
。通常,匯出物件不得使用此方案。
-
Py_ssize_t len¶
product(shape) * itemsize
。對於連續陣列,這是底層記憶體塊的長度。對於非連續陣列,它是如果複製到連續表示形式,邏輯結構將具有的長度。僅當緩衝區透過保證連續性的請求獲得時,訪問
((char *)buf)[0] 至 ((char *)buf)[len-1]
才有效。在大多數情況下,此類請求將是PyBUF_SIMPLE
或PyBUF_WRITABLE
。
-
int readonly¶
指示緩衝區是否為只讀。此欄位由
PyBUF_WRITABLE
標誌控制。
-
Py_ssize_t itemsize¶
單個元素的位元組項大小。與在非
NULL
format
值上呼叫的struct.calcsize()
的值相同。重要例外:如果消費者請求的緩衝區沒有
PyBUF_FORMAT
標誌,則format
將設定為NULL
,但itemsize
仍具有原始格式的值。如果
shape
存在,則等式product(shape) * itemsize == len
仍然成立,消費者可以使用itemsize
來導航緩衝區。如果由於
PyBUF_SIMPLE
或PyBUF_WRITABLE
請求導致shape
為NULL
,則消費者必須忽略itemsize
並假定itemsize == 1
。
-
char *format¶
一個以
struct
模組樣式語法描述單個項內容的 *NULL* 終止字串。如果為NULL
,則假定為"B"
(無符號位元組)。此欄位由
PyBUF_FORMAT
標誌控制。
-
int ndim¶
記憶體作為 n 維陣列表示的維度數。如果為
0
,則buf
指向表示標量的單個項。在這種情況下,shape
、strides
和suboffsets
必須為NULL
。最大維度數由PyBUF_MAX_NDIM
給出。
-
Py_ssize_t *shape¶
一個長度為
Py_ssize_t
的陣列,其長度為ndim
,表示記憶體作為 n 維陣列的形狀。請注意,shape[0] * ... * shape[ndim-1] * itemsize
必須等於len
。形狀值限制為
shape[n] >= 0
。當shape[n] == 0
時需要特別注意。有關更多資訊,請參閱 複雜陣列。形狀陣列對消費者來說是隻讀的。
-
Py_ssize_t *strides¶
一個長度為
Py_ssize_t
的陣列,其長度為ndim
,給出在每個維度中跳過多少位元組才能到達新元素。步幅值可以是任何整數。對於常規陣列,步幅通常為正,但消費者必須能夠處理
strides[n] <= 0
的情況。有關更多資訊,請參閱 複雜陣列。步幅陣列對消費者來說是隻讀的。
-
Py_ssize_t *suboffsets¶
一個長度為
Py_ssize_t
的陣列,其長度為ndim
。如果suboffsets[n] >= 0
,則沿第 n 維儲存的值是指標,並且子偏移量值指示在解引用後要新增到每個指標的位元組數。負的子偏移量值表示不應發生解引用(在連續記憶體塊中跨步)。如果所有子偏移量都為負(即不需要解引用),則此欄位必須為
NULL
(預設值)。這種陣列表示形式由 Python 影像庫 (PIL) 使用。有關如何訪問此類陣列元素的更多資訊,請參閱 複雜陣列。
子偏移量陣列對消費者來說是隻讀的。
-
void *internal¶
這供匯出物件內部使用。例如,匯出器可能會將其重新強制轉換為整數,並用於儲存有關在釋放緩衝區時是否必須釋放形狀、步幅和子偏移量陣列的標誌。消費者不得更改此值。
-
void *buf¶
常量
-
PyBUF_MAX_NDIM¶
記憶體表示的最大維度數。匯出器必須遵守此限制,多維緩衝區的消費者應能處理多達
PyBUF_MAX_NDIM
維。當前設定為 64。
緩衝區請求型別¶
通常透過 PyObject_GetBuffer()
嚮導出物件傳送緩衝區請求來獲取緩衝區。由於記憶體邏輯結構的複雜性可能大相徑庭,消費者使用 *flags* 引數來指定它可以處理的確切緩衝區型別。
所有 Py_buffer
欄位都由請求型別明確定義。
請求無關欄位¶
只讀,格式¶
- PyBUF_WRITABLE¶
控制
readonly
欄位。如果設定,匯出器必須提供可寫緩衝區,否則報告失敗。否則,匯出器可以提供只讀或可寫緩衝區,但選擇必須對所有消費者一致。例如,PyBUF_SIMPLE | PyBUF_WRITABLE 可用於請求簡單的可寫緩衝區。
PyBUF_WRITABLE
可以與下一節中的任何標誌進行或運算。由於 PyBUF_SIMPLE
定義為 0,因此 PyBUF_WRITABLE
可以作為獨立標誌來請求簡單的可寫緩衝區。
PyBUF_FORMAT
必須與除 PyBUF_SIMPLE
之外的任何標誌進行或運算,因為後者已經隱含了格式 B
(無符號位元組)。PyBUF_FORMAT
不能單獨使用。
形狀、步幅、子偏移量¶
控制記憶體邏輯結構的標誌按複雜性遞減的順序排列。請注意,每個標誌都包含其下方所有標誌的所有位。
請求 |
形狀 |
步幅 |
子偏移量 |
---|---|---|---|
|
是 |
是 |
如果需要 |
|
是 |
是 |
NULL |
|
是 |
NULL |
NULL |
|
NULL |
NULL |
NULL |
連續性請求¶
可以明確請求 C 或 Fortran 連續性,帶或不帶步幅資訊。不帶步幅資訊時,緩衝區必須是 C 連續的。
請求 |
形狀 |
步幅 |
子偏移量 |
連續性 |
---|---|---|---|---|
|
是 |
是 |
NULL |
C |
|
是 |
是 |
NULL |
F |
|
是 |
是 |
NULL |
C 或 F |
是 |
NULL |
NULL |
C |
複合請求¶
所有可能的請求都由上一節中的標誌的某些組合完全定義。為方便起見,緩衝區協議提供了常用組合作為單個標誌。
在下表中,*U* 代表未定義的連續性。消費者需要呼叫 PyBuffer_IsContiguous()
來確定連續性。
請求 |
形狀 |
步幅 |
子偏移量 |
連續性 |
只讀 |
format |
---|---|---|---|---|---|---|
|
是 |
是 |
如果需要 |
U |
0 |
是 |
|
是 |
是 |
如果需要 |
U |
1 或 0 |
是 |
|
是 |
是 |
NULL |
U |
0 |
是 |
|
是 |
是 |
NULL |
U |
1 或 0 |
是 |
|
是 |
是 |
NULL |
U |
0 |
NULL |
|
是 |
是 |
NULL |
U |
1 或 0 |
NULL |
|
是 |
NULL |
NULL |
C |
0 |
NULL |
|
是 |
NULL |
NULL |
C |
1 或 0 |
NULL |
複雜陣列¶
NumPy 風格:形狀和步幅¶
NumPy 風格陣列的邏輯結構由 itemsize
、ndim
、shape
和 strides
定義。
如果 ndim == 0
,則 buf
指向的記憶體位置被解釋為大小為 itemsize
的標量。在這種情況下,shape
和 strides
都為 NULL
。
如果 strides
為 NULL
,則陣列被解釋為標準的 n 維 C 陣列。否則,消費者必須按如下方式訪問 n 維陣列:
ptr = (char *)buf + indices[0] * strides[0] + ... + indices[n-1] * strides[n-1];
item = *((typeof(item) *)ptr);
如上所述,buf
可以指向實際記憶體塊內的任何位置。匯出器可以使用此函式檢查緩衝區的有效性:
def verify_structure(memlen, itemsize, ndim, shape, strides, offset):
"""Verify that the parameters represent a valid array within
the bounds of the allocated memory:
char *mem: start of the physical memory block
memlen: length of the physical memory block
offset: (char *)buf - mem
"""
if offset % itemsize:
return False
if offset < 0 or offset+itemsize > memlen:
return False
if any(v % itemsize for v in strides):
return False
if ndim <= 0:
return ndim == 0 and not shape and not strides
if 0 in shape:
return True
imin = sum(strides[j]*(shape[j]-1) for j in range(ndim)
if strides[j] <= 0)
imax = sum(strides[j]*(shape[j]-1) for j in range(ndim)
if strides[j] > 0)
return 0 <= offset+imin and offset+imax+itemsize <= memlen
PIL 風格:形狀、步幅和子偏移量¶
除了常規項之外,PIL 風格的陣列還可以包含必須遵循的指標,以便在維度中獲取下一個元素。例如,常規的三維 C 陣列 char v[2][2][3]
也可以看作是 2 個指向 2 個二維陣列的指標陣列:char (*v[2])[2][3]
。在子偏移量表示中,這兩個指標可以嵌入在 buf
的開頭,指向兩個可以位於記憶體中任何位置的 char x[2][3]
陣列。
以下是一個函式,當步幅和子偏移量都非 NULL
時,它返回指向由 N 維索引指向的 N 維陣列中元素的指標:
void *get_item_pointer(int ndim, void *buf, Py_ssize_t *strides,
Py_ssize_t *suboffsets, Py_ssize_t *indices) {
char *pointer = (char*)buf;
int i;
for (i = 0; i < ndim; i++) {
pointer += strides[i] * indices[i];
if (suboffsets[i] >=0 ) {
pointer = *((char**)pointer) + suboffsets[i];
}
}
return (void*)pointer;
}