緩沖協(xié)議?
在 Python 中可使用一些對象來包裝對底層內(nèi)存數(shù)組或稱 緩沖 的訪問。此類對象包括內(nèi)置的 bytes
和 bytearray
以及一些如 array.array
這樣的擴(kuò)展類型。第三方庫也可能會為了特殊的目的而定義它們自己的類型,例如用于圖像處理和數(shù)值分析等。
雖然這些類型中的每一種都有自己的語義,但它們具有由可能較大的內(nèi)存緩沖區(qū)支持的共同特征。 在某些情況下,希望直接訪問該緩沖區(qū)而無需中間復(fù)制。
Python 以 緩沖協(xié)議 的形式在 C 層級上提供這樣的功能。 此協(xié)議包括兩個方面:
在生產(chǎn)者這一方面,該類型的協(xié)議可以導(dǎo)出一個“緩沖區(qū)接口”,允許公開它的底層緩沖區(qū)信息。該接口的描述信息在 Buffer Object Structures 一節(jié)中;
在消費(fèi)者一側(cè),有幾種方法可用于獲得指向?qū)ο蟮脑嫉讓訑?shù)據(jù)的指針(例如一個方法的形參)。
一些簡單的對象例如 bytes
和 bytearray
會以面向字節(jié)的形式公開它們的底層緩沖區(qū)。 也可能會用其他形式;例如 array.array
所公開的元素可以是多字節(jié)值。
緩沖區(qū)接口的消費(fèi)者的一個例子是文件對象的 write()
方法:任何可以輸出為一系列字節(jié)流的對象可以被寫入文件。然而 write()
方法只需要對于傳入對象的只讀權(quán)限,其他的方法,如 readinto()
需要參數(shù)內(nèi)容的寫入權(quán)限。緩沖區(qū)接口使得對象可以選擇性地允許或拒絕讀寫或只讀緩沖區(qū)的導(dǎo)出。
對于緩沖區(qū)接口的使用者而言,有兩種方式來獲取一個目的對象的緩沖:
使用正確的參數(shù)來調(diào)用
PyObject_GetBuffer()
函數(shù);調(diào)用
PyArg_ParseTuple()
(或其同級對象之一) 并傳入y*
,w*
ors*
格式代碼 中的一個。
在這兩種情況下,當(dāng)不再需要緩沖區(qū)時必須調(diào)用 PyBuffer_Release()
。如果此操作失敗,可能會導(dǎo)致各種問題,例如資源泄漏。
緩沖區(qū)結(jié)構(gòu)?
緩沖區(qū)結(jié)構(gòu)(或者簡單地稱為“buffers”)對于將二進(jìn)制數(shù)據(jù)從另一個對象公開給 Python 程序員非常有用。它們還可以用作零拷貝切片機(jī)制。使用它們引用內(nèi)存塊的能力,可以很容易地將任何數(shù)據(jù)公開給 Python 程序員。內(nèi)存可以是 C 擴(kuò)展中的一個大的常量數(shù)組,也可以是在傳遞到操作系統(tǒng)庫之前用于操作的原始內(nèi)存塊,或者可以用來傳遞本機(jī)內(nèi)存格式的結(jié)構(gòu)化數(shù)據(jù)。
與 Python 解釋器公開的大多部數(shù)據(jù)類型不同,緩沖區(qū)不是 PyObject
指針而是簡單的 C 結(jié)構(gòu)。 這使得它們可以非常簡單地創(chuàng)建和復(fù)制。 當(dāng)需要為緩沖區(qū)加上泛型包裝器時,可以創(chuàng)建一個 內(nèi)存視圖 對象。
有關(guān)如何編寫并導(dǎo)出對象的簡短說明,請參閱 緩沖區(qū)對象結(jié)構(gòu)。 要獲取緩沖區(qū)對象,請參閱 PyObject_GetBuffer()
。
-
type Py_buffer?
- Part of the Stable ABI (including all members) since version 3.11.
-
void *buf?
指向由緩沖區(qū)字段描述的邏輯結(jié)構(gòu)開始的指針。 這可以是導(dǎo)出程序底層物理內(nèi)存塊中的任何位置。 例如,使用負(fù)的
strides
值可能指向內(nèi)存塊的末尾。對于 contiguous ,‘鄰接’數(shù)組,值指向內(nèi)存塊的開頭。
-
void *obj?
對導(dǎo)出對象的新引用。 該引用歸使用者所有,并由
PyBuffer_Release()
自動遞減并設(shè)置為NULL
。 該字段等于任何標(biāo)準(zhǔn) C-API 函數(shù)的返回值。作為一種特殊情況,對于由
PyMemoryView_FromBuffer()
或PyBuffer_FillInfo()
包裝的 temporary 緩沖區(qū),此字段為NULL
。 通常,導(dǎo)出對象不得使用此方案。
-
Py_ssize_t len?
product(shape) * itemsize
。對于連續(xù)數(shù)組,這是基礎(chǔ)內(nèi)存塊的長度。對于非連續(xù)數(shù)組,如果邏輯結(jié)構(gòu)復(fù)制到連續(xù)表示形式,則該長度將具有該長度。僅當(dāng)緩沖區(qū)是通過保證連續(xù)性的請求獲取時,才訪問
((char *)buf)[0] up to ((char *)buf)[len-1]
時才有效。在大多數(shù)情況下,此類請求將為PyBUF_SIMPLE
或PyBUF_WRITABLE
。
-
int readonly?
緩沖區(qū)是否為只讀的指示器。此字段由
PyBUF_WRITABLE
標(biāo)志控制。
-
Py_ssize_t itemsize?
單個元素的項大?。ㄒ宰止?jié)為單位)。與
struct.calcsize()
調(diào)用非NULL
format
的值相同。重要例外:如果使用者請求的緩沖區(qū)沒有
PyBUF_FORMAT
標(biāo)志,format
將設(shè)置為NULL
,但itemsize
仍具有原始格式的值。如果
shape
存在,則相等的product(shape) * itemsize == len
仍然存在,使用者可以使用itemsize
來導(dǎo)航緩沖區(qū)。如果
shape
是NULL
,因為結(jié)果為PyBUF_SIMPLE
或PyBUF_WRITABLE
請求,則使用者必須忽略itemsize
,并假設(shè)itemsize == 1
。
-
const char *format?
在
struct
模塊樣式語法中 NUL 字符串,描述單個項的內(nèi)容。如果這是NULL
,則假定為``"B"`` (無符號字節(jié)) 。此字段由
PyBUF_FORMAT
標(biāo)志控制。
-
int ndim?
內(nèi)存表示為 n 維數(shù)組的維數(shù)。 如果是``0``,
buf
指向表示標(biāo)量的單個項目。 在這種情況下,shape
、strides
和suboffsets
必須是``NULL`` 。宏
PyBUF_MAX_NDIM
將最大維度數(shù)限制為 64。 導(dǎo)出程序必須遵守這個限制,多維緩沖區(qū)的使用者應(yīng)該能夠處理最多PyBUF_MAX_NDIM
維度。
-
Py_ssize_t *shape?
一個長度為
Py_ssize_t
的數(shù)組ndim
表示作為 n 維數(shù)組的內(nèi)存形狀。 請注意,shape[0] * ... * shape[ndim-1] * itemsize
必須等于len
。Shape 形狀數(shù)組中的值被限定在
shape[n] >= 0
。shape[n] == 0
這一情形需要特別注意。更多信息請參閱 complex arrays 。shape 數(shù)組對于使用者來說是只讀的。
-
Py_ssize_t *strides?
一個長度為
Py_ssize_t
的數(shù)組ndim
給出要跳過的字節(jié)數(shù)以獲取每個尺寸中的新元素。Stride 步幅數(shù)組中的值可以為任何整數(shù)。對于常規(guī)數(shù)組,步幅通常為正數(shù),但是使用者必須能夠處理
strides[n] <= 0
的情況。更多信息請參閱 complex arrays 。strides數(shù)組對用戶來說是只讀的。
-
Py_ssize_t *suboffsets?
一個長度為
ndim
類型為Py_ssize_t
的數(shù)組 。如果suboffsets[n] >= 0
,則第 n 維存儲的是指針,suboffset 值決定了解除引用時要給指針增加多少字節(jié)的偏移。suboffset 為負(fù)值,則表示不應(yīng)解除引用(在連續(xù)內(nèi)存塊中移動)。如果所有子偏移均為負(fù)(即無需取消引用),則此字段必須為
NULL
(默認(rèn)值)。Python Imaging Library (PIL) 中使用了這種類型的數(shù)組表達(dá)方式。請參閱 complex arrays 來了解如何從這樣一個數(shù)組中訪問元素。
suboffsets 數(shù)組對于使用者來說是只讀的。
-
void *internal?
供輸出對象內(nèi)部使用。比如可能被輸出程序重組為一個整數(shù),用于存儲一個標(biāo)志,標(biāo)明在緩沖區(qū)釋放時是否必須釋放 shape、strides 和 suboffsets 數(shù)組。消費(fèi)者程序 不得 修改該值。
-
void *buf?
緩沖區(qū)請求的類型?
通常,通過 PyObject_GetBuffer()
向輸出對象發(fā)送緩沖區(qū)請求,即可獲得緩沖區(qū)。由于內(nèi)存的邏輯結(jié)構(gòu)復(fù)雜,可能會有很大差異,緩沖區(qū)使用者可用 flags 參數(shù)指定其能夠處理的緩沖區(qū)具體類型。
所有 Py_buffer
字段均由請求類型明確定義。
與請求無關(guān)的字段?
只讀,格式?
PyBUF_WRITABLE
可以和下一節(jié)的所有標(biāo)志聯(lián)用。由于 PyBUF_SIMPLE
定義為 0,所以 PyBUF_WRITABLE
可以作為一個獨(dú)立的標(biāo)志,用于請求一個簡單的可寫緩沖區(qū)。
PyBUF_FORMAT
可以被設(shè)為除了 PyBUF_SIMPLE
之外的任何標(biāo)志。 后者已經(jīng)按暗示了``B``(無符號字節(jié)串)格式。
形狀,步幅,子偏移量?
控制內(nèi)存邏輯結(jié)構(gòu)的標(biāo)志按照復(fù)雜度的遞減順序列出。注意,每個標(biāo)志包含它下面的所有標(biāo)志。
請求 |
形狀 |
步幅 |
子偏移量 |
---|---|---|---|
|
是 |
是 |
如果需要的話 |
|
是 |
是 |
NULL |
|
是 |
NULL |
NULL |
|
NULL |
NULL |
NULL |
連續(xù)性的請求?
可以顯式地請求C 或 Fortran 連續(xù) ,不管有沒有步幅信息。若沒有步幅信息,則緩沖區(qū)必須是 C-連續(xù)的。
請求 |
形狀 |
步幅 |
子偏移量 |
鄰接 |
---|---|---|---|---|
|
是 |
是 |
NULL |
C |
|
是 |
是 |
NULL |
F |
|
是 |
是 |
NULL |
C 或 F |
是 |
NULL |
NULL |
C |
復(fù)合請求?
所有可能的請求都由上一節(jié)中某些標(biāo)志的組合完全定義。為方便起見,緩沖區(qū)協(xié)議提供常用的組合作為單個標(biāo)志。
在下表中,U 代表連續(xù)性未定義。消費(fèi)者程序必須調(diào)用 PyBuffer_IsContiguous()
以確定連續(xù)性。
請求 |
形狀 |
步幅 |
子偏移量 |
鄰接 |
只讀 |
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 |
復(fù)雜數(shù)組?
NumPy-風(fēng)格:形狀和步幅?
NumPy 風(fēng)格數(shù)組的邏輯結(jié)構(gòu)由 itemsize
、 ndim
、 shape
和 strides
定義。
如果 ndim == 0
, buf
指向的內(nèi)存位置被解釋為大小為 itemsize
的標(biāo)量。這時, shape
和 strides
都為 NULL
。
如果 strides
為 NULL
,則數(shù)組將被解釋為一個標(biāo)準(zhǔn)的 n 維 C 語言數(shù)組。否則,消費(fèi)者程序必須按如下方式訪問 n 維數(shù)組:
ptr = (char *)buf + indices[0] * strides[0] + ... + indices[n-1] * strides[n-1];
item = *((typeof(item) *)ptr);
如上所述,buf
可以指向?qū)嶋H內(nèi)存塊中的任意位置。輸出者程序可以用該函數(shù)檢查緩沖區(qū)的有效性。
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-風(fēng)格:形狀,步幅和子偏移量?
除了常規(guī)項之外, PIL 風(fēng)格的數(shù)組還可以包含指針,必須跟隨這些指針才能到達(dá)維度的下一個元素。例如,常規(guī)的三維 C 語言數(shù)組 char v[2][2][3]
可以看作是一個指向 2 個二維數(shù)組的 2 個指針:char (*v[2])[2][3]
。在子偏移表示中,這兩個指針可以嵌入在 buf
的開頭,指向兩個可以位于內(nèi)存任何位置的 char x[2][3]
數(shù)組。
這是一個函數(shù),當(dāng)n維索引所指向的N-D數(shù)組中有``NULL``步長和子偏移量時,它返回一個指針
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;
}