緩衝區協議

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

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

Python 在 C 和 Python 級別透過 緩衝區協議 提供了這種機制。該協議分為兩方面:

  • 在生產者方面,型別可以匯出一個“緩衝區介面”,允許該型別的物件公開有關其底層緩衝區的資訊。此介面在 緩衝區物件結構 部分中描述;對於 Python,請參閱 模擬緩衝區型別

  • 在消費者方面,有多種方法可以獲取指向物件原始底層資料(例如方法引數)的指標。對於 Python,請參閱 memoryview

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

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

緩衝區介面的消費者有兩種方式獲取目標物件的緩衝區:

在這兩種情況下,當不再需要緩衝區時,必須呼叫 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_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

一個以 struct 模組樣式語法描述單個項內容的 *NULL* 終止字串。如果為 NULL,則假定為 "B"(無符號位元組)。

此欄位由 PyBUF_FORMAT 標誌控制。

int ndim

記憶體作為 n 維陣列表示的維度數。如果為 0,則 buf 指向表示標量的單個項。在這種情況下,shapestridessuboffsets 必須為 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

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

常量

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() 來確定連續性。

請求

形狀

步幅

子偏移量

連續性

只讀

format

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