緩衝區協議

Python 中可用的某些物件封裝了對底層記憶體陣列或緩衝區的訪問。這些物件包括內建的 bytesbytearray,以及一些擴充套件型別,例如 array.array。第三方庫可能會為特殊目的定義自己的型別,例如影像處理或數值分析。

雖然這些型別中的每一種都有自己的語義,但它們都具有由一個可能很大的記憶體緩衝區支援的共同特徵。因此,在某些情況下,希望直接訪問該緩衝區,而無需中間複製。

Python 以 緩衝區協議 的形式在 C 級別提供這種功能。此協議有兩方面

  • 在生產者方面,型別可以匯出“緩衝區介面”,從而使該型別的物件可以公開有關其底層緩衝區的資訊。此介面在 緩衝區物件結構 部分中進行了描述;

  • 在使用者方面,有多種方法可以獲取指向物件原始底層資料的指標(例如,方法引數)。

諸如 bytesbytearray 之類的簡單物件以面向位元組的形式公開其底層緩衝區。其他形式也是可能的;例如,array.array 公開的元素可以是多位元組值。

緩衝區介面的一個示例使用者是檔案物件的 write() 方法:任何可以透過緩衝區介面匯出位元組序列的物件都可以寫入檔案。雖然 write() 僅需要對傳遞給它的物件的內部內容進行只讀訪問,但其他方法(例如 readinto())則需要對其引數的內容進行寫入訪問。緩衝區介面允許物件有選擇地允許或拒絕匯出讀寫和只讀緩衝區。

緩衝區介面的使用者有兩種方法來獲取目標物件的緩衝區

在這兩種情況下,不再需要緩衝區時都必須呼叫 PyBuffer_Release()。 否則可能會導致諸如資源洩漏之類的各種問題。

緩衝區結構

緩衝區結構(或簡稱為“緩衝區”)作為一種將來自另一個物件的二進位制資料暴露給 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_SIMPLEPyBUF_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_SIMPLEPyBUF_WRITABLE 請求導致 shapeNULL,則使用者必須忽略 itemsize 並假設 itemsize == 1

char *format

一個以 NULL 結尾的字串,採用 struct 模組風格語法,描述單個專案的內容。如果此值為 NULL,則假設為 "B"(無符號位元組)。

此欄位由 PyBUF_FORMAT 標誌控制。

int ndim

記憶體表示為 n 維陣列的維度數。如果為 0,則 buf 指向表示標量的單個專案。在這種情況下,shapestridessuboffsets 必須為 NULL。最大維度數由 PyBUF_MAX_NDIM 給出。

Py_ssize_t *shape

一個長度為 ndimPy_ssize_t 陣列,表示記憶體作為 n 維陣列的形狀。請注意,shape[0] * ... * shape[ndim-1] * itemsize 必須等於 len

形狀值被限制為 shape[n] >= 0shape[n] == 0 的情況需要特別注意。有關更多資訊,請參閱複雜陣列

形狀陣列對於使用者是隻讀的。

Py_ssize_t *strides

一個長度為 ndimPy_ssize_t 陣列,給出在每個維度中跳過多少位元組才能到達新元素。

步長值可以是任何整數。對於規則陣列,步長通常為正數,但是使用者必須能夠處理 strides[n] <= 0 的情況。有關更多資訊,請參閱複雜陣列

步長陣列對於使用者是隻讀的。

Py_ssize_t *suboffsets

一個長度為 ndimPy_ssize_t 陣列。如果 suboffsets[n] >= 0,則沿第 n 個維度儲存的值是指標,並且子偏移值決定在取消引用後向每個指標新增多少位元組。負的子偏移值表示不應發生取消引用(在連續的記憶體塊中步進)。

如果所有子偏移量均為負數(即不需要取消引用),則此欄位必須為 NULL(預設值)。

Python Imaging Library (PIL) 使用這種型別的陣列表示形式。有關如何訪問此類陣列的元素,請參閱複雜陣列

子偏移陣列對於使用者是隻讀的。

void *internal

這供匯出物件在內部使用。例如,匯出器可能會將其重新轉換為整數,並用於儲存有關是否必須在釋放緩衝區時釋放形狀、步長和子偏移陣列的標誌。使用者不得更改此值。

常量

PyBUF_MAX_NDIM

記憶體表示的最大維度數。匯出器必須遵守此限制,多維緩衝區的使用者應該能夠處理多達 PyBUF_MAX_NDIM 個維度。當前設定為 64。

緩衝區請求型別

緩衝區通常透過 PyObject_GetBuffer() 嚮導出物件傳送緩衝區請求來獲取。由於記憶體的邏輯結構的複雜性可能差異很大,因此使用者使用 flags 引數來指定它可以處理的確切緩衝區型別。

所有 Py_buffer 欄位都由請求型別明確定義。

與請求無關的欄位

以下欄位不受 flags 的影響,並且必須始終使用正確的值填充:objbuflenitemsizendim

只讀,格式

PyBUF_WRITABLE

控制 readonly 欄位。如果設定,匯出器必須提供可寫緩衝區,否則報告失敗。否則,匯出器可以提供只讀或可寫緩衝區,但對於所有使用者,選擇必須一致。例如,PyBUF_SIMPLE | PyBUF_WRITABLE 可用於請求簡單的可寫緩衝區。

PyBUF_FORMAT

控制 format 欄位。如果設定,則必須正確填寫此欄位。否則,此欄位必須為 NULL

PyBUF_WRITABLE 可以與下一節中的任何標誌進行“或”運算。由於 PyBUF_SIMPLE 定義為 0,因此 PyBUF_WRITABLE 可以用作獨立標誌來請求簡單的可寫緩衝區。

PyBUF_FORMAT 必須與除了 PyBUF_SIMPLE 之外的任何標誌進行“或”運算,因為後者已經隱含了格式 B (無符號位元組)。 PyBUF_FORMAT 不能單獨使用。

形狀、步長、子偏移量

控制記憶體邏輯結構的標誌按複雜程度遞減的順序列出。請注意,每個標誌都包含其下方標誌的所有位。

請求

形狀

步長

子偏移量

PyBUF_INDIRECT

如果需要

PyBUF_STRIDES

NULL

PyBUF_ND

NULL

NULL

PyBUF_SIMPLE

NULL

NULL

NULL

連續性請求

可以顯式請求 C 或 Fortran 連續性,無論是否具有步長資訊。如果沒有步長資訊,則緩衝區必須是 C 連續的。

請求

形狀

步長

子偏移量

連續

PyBUF_C_CONTIGUOUS

NULL

C

PyBUF_F_CONTIGUOUS

NULL

F

PyBUF_ANY_CONTIGUOUS

NULL

C 或 F

PyBUF_ND

NULL

NULL

C

複合請求

所有可能的請求都完全由上一節中標誌的某種組合定義。為了方便起見,緩衝區協議提供了常用組合作為單個標誌。

在下表中,U 表示未定義的連續性。使用者必須呼叫 PyBuffer_IsContiguous() 來確定連續性。

請求

形狀

步長

子偏移量

連續

只讀

格式

PyBUF_FULL

如果需要

U

0

PyBUF_FULL_RO

如果需要

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 樣式:形狀和步長

NumPy 樣式陣列的邏輯結構由 itemsizendimshapestrides 定義。

如果 ndim == 0,則 buf 指向的記憶體位置被解釋為大小為 itemsize 的標量。在這種情況下,shapestrides 均為 NULL

如果 stridesNULL,則陣列被解釋為標準的 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;
}