バッファプロトコル (buffer Protocol)

Pythonで利用可能ないくつかのオブジェクトは、下層にあるメモリ配列または buffer へのアクセスを提供します。このようなオブジェクトとして、組み込みの bytesbytearrayarray.array のようないくつかの拡張型が挙げられます。サードバーティのライブラリは画像処理や数値解析のような特別な目的のために、それら自身の型を定義することができます。

それぞれの型はそれ自身のセマンティクスを持ちますが、おそらく大きなメモリバッファからなるという共通の特徴を共有します。いくつかの状況では仲介するコピーを行うことなく直接バッファにアクセスすることが望まれます。

Pythonは buffer protocol の形式で C レベルの仕組みを提供します。このプロトコルには二つの側面があります:

  • 提供する側では、ある型は、そのオブジェクトの下層にあるバッファに関する情報を提供できる "buffer インターフェース" をエクスポートすることができます。このインターフェースは バッファオブジェクト構造体 (buffer object structure) の節で説明します。

  • 利用する側では、オブジェクトの下層にある生データへのポインタを得るいくつかの手段が利用できます(たとえばメソッド引数)。

bytesbytearray などのシンプルなオブジェクトは、内部のバッファーをバイト列の形式で公開します。 バイト列以外の形式も利用可能です。例えば、 array.array が公開する要素はマルチバイト値になることがあります。

bufferインターフェースの利用者の一例は、ファイルオブジェクトの write() メソッドです: bufferインターフェースを通して一連のバイト列を提供できるどんなオブジェクトでもファイルに書き込むことができます。 write() は、その引数として渡されたオブジェクトの内部要素に対する読み出し専用アクセスのみを必要としますが、 readinto() のような他のメソッドでは、その引数の内容に対する書き込みアクセスが必要です。bufferインターフェースにより、オブジェクトは読み書き両方、読み出し専用バッファへのアクセスを許可するかそれとも拒否するか選択することができます。

bufferインターフェースの利用者には、対象となるオブジェクトのバッファを得る二つの方法があります:

どちらのケースでも、bufferが必要なくなった時に PyBuffer_Release() を呼び出さなければなりません。これを怠ると、リソースリークのような様々な問題につながる恐れがあります。

buffer 構造体

バッファ構造体(または単純に "buffers")は別のオブジェクトのバイナリデータをPythonプログラマに提供するのに便利です。これはまた、ゼロコピースライシング機構としても使用できます。このメモリブロックを参照する機能を使うことで、どんなデータでもとても簡単にPythonプログラマに提供することができます。メモリは、C 拡張の大きな配列定数かもしれませんし、オペレーティングシステムライブラリに渡す前のメモリブロックかもしれませんし、構造化データをネイティブのインメモリ形式受け渡すのに使用されるかもしれません。

Pythonインタプリタによって提供される多くのデータ型とは異なり、バッファは PyObject ポインタではなく、シンプルなC 構造体です。そのため、作成とコピーが非常に簡単に行えます。バッファの一般的なラッパーが必要なときは、 memoryview オブジェクトが作成されます。

エクスポートされるオブジェクトを書く方法の短い説明には、 Buffer Object Structures を参照してください。バッファを取得するには、 PyObject_GetBuffer() を参照してください。

type Py_buffer
次に属します: Stable ABI (すべてのメンバーを含む) (バージョン 3.11 より).
void *buf

バッファフィールドが表している論理構造の先頭を指すポインタ。 バッファを提供するオブジェクトの下層物理メモリブロック中のどの位置にもなりえます。 例えば strides が負だと、この値はメモリブロックの末尾かもしれません。

連続 配列の場合この値はメモリブロックの先頭を指します。

PyObject *obj

A new reference to the exporting object. The reference is owned by the consumer and automatically released (i.e. reference count decremented) and set to NULL by PyBuffer_Release(). The field is the equivalent of the return value of any standard C-API function.

PyMemoryView_FromBuffer() または PyBuffer_FillInfo() によってラップされた 一時的な バッファである特別なケースでは、このフィールドは NULL です。一般的に、エクスポートオブジェクトはこの方式を使用してはなりません。

Py_ssize_t len

product(shape) * itemsize。contiguous配列では、下層のメモリブロックの長さになります。非contiguous 配列では、contiguous表現にコピーされた場合に論理構造がもつ長さです。

((char *)buf)[0] から ((char *)buf)[len-1] の範囲へのアクセスは、連続性 (contiguity) を保証するリクエストによって取得されたバッファに対してのみ許されます。 多くの場合に、そのようなリクエストは PyBUF_SIMPLE または PyBUF_WRITABLE です。

int readonly

バッファが読み出し専用であるか示します。このフィールドは PyBUF_WRITABLE フラグで制御できます。

Py_ssize_t itemsize

要素一つ分のbyte単位のサイズ。 struct.calcsize() を非 NULLformat 値に対して呼び出した結果と同じです。

重要な例外: 消費者が PyBUF_FORMAT フラグを設定することなくバッファを要求した場合、 formatNULL に設定されます。 しかし itemsize は元のフォーマットに従った値を保持します。

shape が存在する場合、 product(shape) * itemsize == len の等式が守られ、利用者は itemsize を buffer を読むために利用できます。

PyBUF_SIMPLE または PyBUF_WRITABLE で要求した結果、 shapeNULL であれば、消費者は itemsize を無視して itemsize == 1 と見なさなければなりません。

char *format

A NULL terminated string in struct module style syntax describing the contents of a single item. If this is NULL, "B" (unsigned bytes) is assumed.

このフィールドは PyBUF_FORMAT フラグによって制御されます。

int ndim

The number of dimensions the memory represents as an n-dimensional array. If it is 0, buf points to a single item representing a scalar. In this case, shape, strides and suboffsets MUST be NULL. The maximum number of dimensions is given by PyBUF_MAX_NDIM.

Py_ssize_t *shape

メモリ上のN次元配列の形を示す、長さが ndim である Py_ssize_t の配列です。 shape[0] * ... * shape[ndim-1] * itemsizelen と等しくなければなりません。

shape の値は shape[n] >= 0 に制限されます。 shape[n] == 0 の場合に特に注意が必要です。 詳細は complex arrays を参照してください。

shepe (形状) 配列は利用者からは読み出し専用です。

Py_ssize_t *strides

各次元において新しい値を得るためにスキップするバイト数を示す、長さ ndimPy_ssize_t の配列。

ストライド値は、任意の整数を指定できます。規定の配列では、ストライドは通常でいけば有効です。しかし利用者は、strides[n] <= 0 のケースを処理することができる必要があります。詳細については complex arrays を参照してください。

消費者にとって、この strides 配列は読み出し専用です。

Py_ssize_t *suboffsets

Py_ssize_t 型の要素を持つ長さ ndim の配列。 suboffsets[n] >= 0 の場合は、 n 番目の次元に沿って保存されている値はポインタで、 suboffset 値は各ポインタの参照を解決した後に何バイト加えればいいかを示しています。 suboffset の値が負の数の場合は、ポインタの参照解決は不要 (連続したメモリブロック内に直接配置されいる) ということになります。

全ての suboffset が負数の場合 (つまり参照解決が不要) な場合、このフィールドは NULL (デフォルト値) でなければなりません。

この種の配列表現は Python Imaging Library (PIL) で使われています。 このような配列で要素にアクセスする方法についてさらに詳しことは complex arrays を参照してください。

消費者にとって、suboffsets 配列は読み出し専用です。

void *internal

バッファを提供する側のオブジェクトが内部的に利用するための変数です。例えば、提供側はこの変数に整数型をキャストして、shape, strides, suboffsets といった配列をバッファを開放するときに同時に解放するべきかどうかを管理するフラグに使うことができるでしょう。バッファを受け取る側は、この値を決して変更してはなりません。

Constants:

PyBUF_MAX_NDIM

The maximum number of dimensions the memory represents. Exporters MUST respect this limit, consumers of multi-dimensional buffers SHOULD be able to handle up to PyBUF_MAX_NDIM dimensions. Currently set to 64.

バッファリクエストのタイプ

バッファは通常、 PyObject_GetBuffer() を使うことで、エクスポートするオブジェクトにバッファリクエストを送ることで得られます。メモリの論理的な構造の複雑性は多岐にわたるため、消費者は flags 引数を使って、自身が扱えるバッファの種類を指定します。

Py_buffer の全フィールドは、リクエストの種類によって曖昧さを残さずに定義されます。

リクエストに依存しないフィールド

下記のフィールドは flags の影響を受けずに、常に正しい値で設定されます。: obj, buf, len, itemsize, ndim.

readonly, format

PyBUF_WRITABLE

Controls the readonly field. If set, the exporter MUST provide a writable buffer or else report failure. Otherwise, the exporter MAY provide either a read-only or writable buffer, but the choice MUST be consistent for all consumers. For example, PyBUF_SIMPLE | PyBUF_WRITABLE can be used to request a simple writable buffer.

PyBUF_FORMAT

format フィールドを制御します。もしフラグが設定されていれば、このフィールドを正しく埋めなければなりません。フラグが設定されていなければ、このフィールドを NULL に設定しなければなりません。

PyBUF_WRITABLE は、次の節に出てくるどのフラグとも | を取ってかまいません。 PyBUF_SIMPLE は 0 と定義されているので、 PyBUF_WRITABLE は単純な書き込み可能なバッファを要求する単独のフラグとして使えます。

PyBUF_FORMAT must be |'d to any of the flags except PyBUF_SIMPLE, because the latter already implies format B (unsigned bytes). PyBUF_FORMAT cannot be used on its own.

shape, strides, suboffsets

このフラグは、以下で複雑性が大きい順に並べたメモリの論理的な構造を制御します。個々のフラグは、それより下に記載されたフラグのすべてのビットを含むことに注意してください。

リクエスト

shape

strides

suboffsets

PyBUF_INDIRECT

yes

yes

必要な場合

PyBUF_STRIDES

yes

yes

NULL

PyBUF_ND

yes

NULL

NULL

PyBUF_SIMPLE

NULL

NULL

NULL

隣接性のリクエスト

ストライドの情報があってもなくても、C または Fortran の 連続性 が明確に要求される可能性があります。 ストライド情報なしに、バッファーは C と隣接している必要があります。

リクエスト

shape

strides

suboffsets

contig

PyBUF_C_CONTIGUOUS

yes

yes

NULL

C

PyBUF_F_CONTIGUOUS

yes

yes

NULL

F

PyBUF_ANY_CONTIGUOUS

yes

yes

NULL

C か F

PyBUF_ND

yes

NULL

NULL

C

複合リクエスト

有り得る全てのリクエストの値は、前の節でのフラグの組み合わせで網羅的に定義されています。 便利なように、バッファープロトコルでは頻繁に使用される組み合わせを単一のフラグとして提供してます。

次のテーブルの U は連続性が未定義であることを表します。 利用者は PyBuffer_IsContiguous() を呼び出して連続性を判定する必要があるでしょう。

リクエスト

shape

strides

suboffsets

contig

readonly

format

PyBUF_FULL

yes

yes

必要な場合

U

0

yes

PyBUF_FULL_RO

yes

yes

必要な場合

U

1 か 0

yes

PyBUF_RECORDS

yes

yes

NULL

U

0

yes

PyBUF_RECORDS_RO

yes

yes

NULL

U

1 か 0

yes

PyBUF_STRIDED

yes

yes

NULL

U

0

NULL

PyBUF_STRIDED_RO

yes

yes

NULL

U

1 か 0

NULL

PyBUF_CONTIG

yes

NULL

NULL

C

0

NULL

PyBUF_CONTIG_RO

yes

NULL

NULL

C

1 か 0

NULL

複雑な配列

NumPy スタイル: shape, strides

NumPy スタイルの配列の論理的構造は itemsize, ndim, shape, strides で定義されます。

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 スタイル: shape, strides, suboffsets

PIL スタイルの配列では通常の要素の他に、ある次元の上で次の要素を取得するために辿るポインタを持てます。 例えば、通常の3次元 C 配列 char v[2][2][3] は、2次元配列への 2 つのポインタからなる配列 char (*v[2])[2][3] と見ることもできます。 suboffset 表現では、これらの 2 つのポインタは buf の先頭に埋め込め、メモリのどこにでも配置できる 2 つの char x[2][3] 配列を指します。

次の例は、 strides も suboffsets も 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;
}