緩沖協(xié)議?

在 Python 中可使用一些對象來(lái)包裝對底層內存數組或稱(chēng) 緩沖 的訪(fǎng)問(wèn)。此類(lèi)對象包括內置的 bytesbytearray 以及一些如 array.array 這樣的擴展類(lèi)型。第三方庫也可能會(huì )為了特殊的目的而定義它們自己的類(lèi)型,例如用于圖像處理和數值分析等。

雖然這些類(lèi)型中的每一種都有自己的語(yǔ)義,但它們具有由可能較大的內存緩沖區支持的共同特征。 在某些情況下,希望直接訪(fǎng)問(wèn)該緩沖區而無(wú)需中間復制。

Python 以 緩沖協(xié)議 的形式在 C 層級上提供這樣的功能。 此協(xié)議包括兩個(gè)方面:

  • 在生產(chǎn)者這一方面,該類(lèi)型的協(xié)議可以導出一個(gè)“緩沖區接口”,允許公開(kāi)它的底層緩沖區信息。該接口的描述信息在 Buffer Object Structures 一節中;

  • 在消費者一側,有幾種方法可用于獲得指向對象的原始底層數據的指針(例如一個(gè)方法的形參)。

一些簡(jiǎn)單的對象例如 bytesbytearray 會(huì )以面向字節的形式公開(kāi)它們的底層緩沖區。 也可能會(huì )用其他形式;例如 array.array 所公開(kāi)的元素可以是多字節值。

緩沖區接口的消費者的一個(gè)例子是文件對象的 write() 方法:任何可以輸出為一系列字節流的對象可以被寫(xiě)入文件。然而 write() 方法只需要對于傳入對象的只讀權限,其他的方法,如 readinto() 需要參數內容的寫(xiě)入權限。緩沖區接口使得對象可以選擇性地允許或拒絕讀寫(xiě)或只讀緩沖區的導出。

對于緩沖區接口的使用者而言,有兩種方式來(lái)獲取一個(gè)目的對象的緩沖:

在這兩種情況下,當不再需要緩沖區時(shí)必須調用 PyBuffer_Release() 。如果此操作失敗,可能會(huì )導致各種問(wèn)題,例如資源泄漏。

緩沖區結構?

緩沖區結構(或者簡(jiǎn)單地稱(chēng)為“buffers”)對于將二進(jìn)制數據從另一個(gè)對象公開(kāi)給 Python 程序員非常有用。它們還可以用作零拷貝切片機制。使用它們引用內存塊的能力,可以很容易地將任何數據公開(kāi)給 Python 程序員。內存可以是 C 擴展中的一個(gè)大的常量數組,也可以是在傳遞到操作系統庫之前用于操作的原始內存塊,或者可以用來(lái)傳遞本機內存格式的結構化數據。

與 Python 解釋器公開(kāi)的大多部數據類(lèi)型不同,緩沖區不是 PyObject 指針而是簡(jiǎn)單的 C 結構。 這使得它們可以非常簡(jiǎn)單地創(chuàng )建和復制。 當需要為緩沖區加上泛型包裝器時(shí),可以創(chuàng )建一個(gè) 內存視圖 對象。

有關(guān)如何編寫(xiě)并導出對象的簡(jiǎn)短說(shuō)明,請參閱 緩沖區對象結構。 要獲取緩沖區對象,請參閱 PyObject_GetBuffer()。

type Py_buffer?
Part of the Stable ABI (including all members) since version 3.11.
void *buf?

指向由緩沖區字段描述的邏輯結構開(kāi)始的指針。 這可以是導出程序底層物理內存塊中的任何位置。 例如,使用負的 strides 值可能指向內存塊的末尾。

對于 contiguous ,‘鄰接’數組,值指向內存塊的開(kāi)頭。

void *obj?

對導出對象的新引用。 該引用歸使用者所有,并由 PyBuffer_Release() 自動(dòng)遞減并設置為 NULL。 該字段等于任何標準 C-API 函數的返回值。

作為一種特殊情況,對于由 PyMemoryView_FromBuffer()PyBuffer_FillInfo() 包裝的 temporary 緩沖區,此字段為 NULL。 通常,導出對象不得使用此方案。

Py_ssize_t len?

product(shape) * itemsize。對于連續數組,這是基礎內存塊的長(cháng)度。對于非連續數組,如果邏輯結構復制到連續表示形式,則該長(cháng)度將具有該長(cháng)度。

僅當緩沖區是通過(guò)保證連續性的請求獲取時(shí),才訪(fǎng)問(wèn) ((char *)buf)[0] up to ((char *)buf)[len-1] 時(shí)才有效。在大多數情況下,此類(lèi)請求將為 PyBUF_SIMPLEPyBUF_WRITABLE。

int readonly?

緩沖區是否為只讀的指示器。此字段由 PyBUF_WRITABLE 標志控制。

Py_ssize_t itemsize?

單個(gè)元素的項大?。ㄒ宰止潪閱挝唬?。與 struct.calcsize() 調用非 NULL format 的值相同。

重要例外:如果使用者請求的緩沖區沒(méi)有 PyBUF_FORMAT 標志,format 將設置為 NULL,但 itemsize 仍具有原始格式的值。

如果 shape 存在,則相等的 product(shape) * itemsize == len 仍然存在,使用者可以使用 itemsize 來(lái)導航緩沖區。

如果 shapeNULL,因為結果為 PyBUF_SIMPLEPyBUF_WRITABLE 請求,則使用者必須忽略 itemsize,并假設 itemsize == 1。

const char *format?

struct 模塊樣式語(yǔ)法中 NUL 字符串,描述單個(gè)項的內容。如果這是 NULL,則假定為``"B"`` (無(wú)符號字節) 。

此字段由 PyBUF_FORMAT 標志控制。

int ndim?

內存表示為 n 維數組的維數。 如果是``0``, buf 指向表示標量的單個(gè)項目。 在這種情況下,shape、stridessuboffsets 必須是``NULL`` 。

PyBUF_MAX_NDIM 將最大維度數限制為 64。 導出程序必須遵守這個(gè)限制,多維緩沖區的使用者應該能夠處理最多 PyBUF_MAX_NDIM 維度。

Py_ssize_t *shape?

一個(gè)長(cháng)度為 Py_ssize_t 的數組 ndim 表示作為 n 維數組的內存形狀。 請注意,shape[0] * ... * shape[ndim-1] * itemsize 必須等于 len。

Shape 形狀數組中的值被限定在 shape[n] >= 0 。 shape[n] == 0 這一情形需要特別注意。更多信息請參閱 complex arrays 。

shape 數組對于使用者來(lái)說(shuō)是只讀的。

Py_ssize_t *strides?

一個(gè)長(cháng)度為 Py_ssize_t 的數組 ndim 給出要跳過(guò)的字節數以獲取每個(gè)尺寸中的新元素。

Stride 步幅數組中的值可以為任何整數。對于常規數組,步幅通常為正數,但是使用者必須能夠處理 strides[n] <= 0 的情況。更多信息請參閱 complex arrays 。

strides數組對用戶(hù)來(lái)說(shuō)是只讀的。

Py_ssize_t *suboffsets?

一個(gè)長(cháng)度為 ndim 類(lèi)型為 Py_ssize_t 的數組 。如果 suboffsets[n] >= 0,則第 n 維存儲的是指針,suboffset 值決定了解除引用時(shí)要給指針增加多少字節的偏移。suboffset 為負值,則表示不應解除引用(在連續內存塊中移動(dòng))。

如果所有子偏移均為負(即無(wú)需取消引用),則此字段必須為 NULL (默認值)。

Python Imaging Library (PIL) 中使用了這種類(lèi)型的數組表達方式。請參閱 complex arrays 來(lái)了解如何從這樣一個(gè)數組中訪(fǎng)問(wèn)元素。

suboffsets 數組對于使用者來(lái)說(shuō)是只讀的。

void *internal?

供輸出對象內部使用。比如可能被輸出程序重組為一個(gè)整數,用于存儲一個(gè)標志,標明在緩沖區釋放時(shí)是否必須釋放 shape、strides 和 suboffsets 數組。消費者程序 不得 修改該值。

緩沖區請求的類(lèi)型?

通常,通過(guò) PyObject_GetBuffer() 向輸出對象發(fā)送緩沖區請求,即可獲得緩沖區。由于內存的邏輯結構復雜,可能會(huì )有很大差異,緩沖區使用者可用 flags 參數指定其能夠處理的緩沖區具體類(lèi)型。

所有 Py_buffer 字段均由請求類(lèi)型明確定義。

與請求無(wú)關(guān)的字段?

以下字段不會(huì )被 flags 影響,并且必須總是用正確的值填充:obj, buf,len,itemsize,ndim。

只讀,格式?

PyBUF_WRITABLE?

控制 readonly 字段。如果設置了,輸出程序 必須 提供一個(gè)可寫(xiě)的緩沖區,否則報告失敗。若未設置,輸出程序 可以 提供只讀或可寫(xiě)的緩沖區,但對所有消費者程序 必須 保持一致。

PyBUF_FORMAT?

控制 format 字段。 如果設置,則必須正確填寫(xiě)此字段。其他情況下,此字段必須為``NULL``。

PyBUF_WRITABLE 可以和下一節的所有標志聯(lián)用。由于 PyBUF_SIMPLE 定義為 0,所以 PyBUF_WRITABLE 可以作為一個(gè)獨立的標志,用于請求一個(gè)簡(jiǎn)單的可寫(xiě)緩沖區。

PyBUF_FORMAT 可以被設為除了 PyBUF_SIMPLE 之外的任何標志。 后者已經(jīng)按暗示了``B``(無(wú)符號字節串)格式。

形狀,步幅,子偏移量?

控制內存邏輯結構的標志按照復雜度的遞減順序列出。注意,每個(gè)標志包含它下面的所有標志。

請求

形狀

步幅

子偏移量

PyBUF_INDIRECT?

如果需要的話(huà)

PyBUF_STRIDES?

NULL

PyBUF_ND?

NULL

NULL

PyBUF_SIMPLE?

NULL

NULL

NULL

連續性的請求?

可以顯式地請求C 或 Fortran 連續 ,不管有沒(méi)有步幅信息。若沒(méi)有步幅信息,則緩沖區必須是 C-連續的。

請求

形狀

步幅

子偏移量

鄰接

PyBUF_C_CONTIGUOUS?

NULL

C

PyBUF_F_CONTIGUOUS?

NULL

F

PyBUF_ANY_CONTIGUOUS?

NULL

C 或 F

PyBUF_ND

NULL

NULL

C

復合請求?

所有可能的請求都由上一節中某些標志的組合完全定義。為方便起見(jiàn),緩沖區協(xié)議提供常用的組合作為單個(gè)標志。

在下表中,U 代表連續性未定義。消費者程序必須調用 PyBuffer_IsContiguous() 以確定連續性。

請求

形狀

步幅

子偏移量

鄰接

只讀

format

PyBUF_FULL?

如果需要的話(huà)

U

0

PyBUF_FULL_RO?

如果需要的話(huà)

U

1 或 0

PyBUF_RECORDS?

NULL

U

0

PyBUF_RECORDS_RO?

NULL

U

1 或 0

PyBUF_STRIDED?

NULL

U

0

NULL

PyBUF_STRIDED_RO?

NULL

U

1 或 0

NULL

PyBUF_CONTIG?

NULL

NULL

C

0

NULL

PyBUF_CONTIG_RO?

NULL

NULL

C

1 或 0

NULL

復雜數組?

NumPy-風(fēng)格:形狀和步幅?

NumPy 風(fēng)格數組的邏輯結構由 itemsize 、 ndim 、 shapestrides 定義。

如果 ndim == 0 , buf 指向的內存位置被解釋為大小為 itemsize 的標量。這時(shí), shapestrides 都為 NULL。

如果 stridesNULL,則數組將被解釋為一個(gè)標準的 n 維 C 語(yǔ)言數組。否則,消費者程序必須按如下方式訪(fǎng)問(wèn) n 維數組:

ptr = (char *)buf + indices[0] * strides[0] + ... + indices[n-1] * strides[n-1];
item = *((typeof(item) *)ptr);

如上所述,buf 可以指向實(shí)際內存塊中的任意位置。輸出者程序可以用該函數檢查緩沖區的有效性。

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)格:形狀,步幅和子偏移量?

除了常規項之外, PIL 風(fēng)格的數組還可以包含指針,必須跟隨這些指針才能到達維度的下一個(gè)元素。例如,常規的三維 C 語(yǔ)言數組 char v[2][2][3] 可以看作是一個(gè)指向 2 個(gè)二維數組的 2 個(gè)指針:char (*v[2])[2][3]。在子偏移表示中,這兩個(gè)指針可以嵌入在 buf 的開(kāi)頭,指向兩個(gè)可以位于內存任何位置的 char x[2][3] 數組。

這是一個(gè)函數,當n維索引所指向的N-D數組中有``NULL``步長(cháng)和子偏移量時(shí),它返回一個(gè)指針

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;
}